blob: 936d1ac5114bbdd820719e7cbcfccd5bbb290c73 [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
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
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
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
szager@chromium.org151ebcf2016-03-09 01:08:25 +000050import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000052import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000053import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000054import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000056import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import watchlists
61
maruel@chromium.org0633fb42013-08-16 20:06:14 +000062__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000064DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000065POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000067GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000256 error = content_json.get('error')
257 if error.get('code') == 403:
258 raise BuildbucketResponseException(
259 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000260 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000261 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 raise BuildbucketResponseException(msg)
263
264 if response.status == 200:
265 if not content_json:
266 raise BuildbucketResponseException(
267 'Buildbucket returns invalid json content: %s.\n'
268 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
269 content)
270 return content_json
271 if response.status < 500 or try_count >= 2:
272 raise httplib2.HttpLib2Error(content)
273
274 # status >= 500 means transient failures.
275 logging.debug('Transient errors when %s. Will retry.', operation_name)
276 time.sleep(0.5 + 1.5*try_count)
277 try_count += 1
278 assert False, 'unreachable'
279
280
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000281def trigger_luci_job(changelist, masters, options):
282 """Send a job to run on LUCI."""
283 issue_props = changelist.GetIssueProperties()
284 issue = changelist.GetIssue()
285 patchset = changelist.GetMostRecentPatchset()
286 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000287 # TODO(hinoka et al): add support for other properties.
288 # Currently, this completely ignores testfilter and other properties.
289 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000290 luci_trigger.trigger(
291 builder, 'HEAD', issue, patchset, issue_props['project'])
292
293
machenbach@chromium.org45453142015-09-15 08:45:22 +0000294def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000295 rietveld_url = settings.GetDefaultServerUrl()
296 rietveld_host = urlparse.urlparse(rietveld_url).hostname
297 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
298 http = authenticator.authorize(httplib2.Http())
299 http.force_exception_to_status_code = True
300 issue_props = changelist.GetIssueProperties()
301 issue = changelist.GetIssue()
302 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000303 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305 buildbucket_put_url = (
306 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000307 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000308 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
309 hostname=rietveld_host,
310 issue=issue,
311 patch=patchset)
312
313 batch_req_body = {'builds': []}
314 print_text = []
315 print_text.append('Tried jobs on:')
316 for master, builders_and_tests in sorted(masters.iteritems()):
317 print_text.append('Master: %s' % master)
318 bucket = _prefix_master(master)
319 for builder, tests in sorted(builders_and_tests.iteritems()):
320 print_text.append(' %s: %s' % (builder, tests))
321 parameters = {
322 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000323 'changes': [{
324 'author': {'email': issue_props['owner_email']},
325 'revision': options.revision,
326 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000327 'properties': {
328 'category': category,
329 'issue': issue,
330 'master': master,
331 'patch_project': issue_props['project'],
332 'patch_storage': 'rietveld',
333 'patchset': patchset,
334 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 },
337 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000338 if tests:
339 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000340 if properties:
341 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342 if options.clobber:
343 parameters['properties']['clobber'] = True
344 batch_req_body['builds'].append(
345 {
346 'bucket': bucket,
347 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000348 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349 'tags': ['builder:%s' % builder,
350 'buildset:%s' % buildset,
351 'master:%s' % master,
352 'user_agent:git_cl_try']
353 }
354 )
355
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000356 _buildbucket_retry(
357 'triggering tryjobs',
358 http,
359 buildbucket_put_url,
360 'PUT',
361 body=json.dumps(batch_req_body),
362 headers={'Content-Type': 'application/json'}
363 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000364 print_text.append('To see results here, run: git cl try-results')
365 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000367
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000369def fetch_try_jobs(auth_config, changelist, options):
370 """Fetches tryjobs from buildbucket.
371
372 Returns a map from build id to build info as json dictionary.
373 """
374 rietveld_url = settings.GetDefaultServerUrl()
375 rietveld_host = urlparse.urlparse(rietveld_url).hostname
376 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
377 if authenticator.has_cached_credentials():
378 http = authenticator.authorize(httplib2.Http())
379 else:
380 print ('Warning: Some results might be missing because %s' %
381 # Get the message on how to login.
382 auth.LoginRequiredError(rietveld_host).message)
383 http = httplib2.Http()
384
385 http.force_exception_to_status_code = True
386
387 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
388 hostname=rietveld_host,
389 issue=changelist.GetIssue(),
390 patch=options.patchset)
391 params = {'tag': 'buildset:%s' % buildset}
392
393 builds = {}
394 while True:
395 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
396 hostname=options.buildbucket_host,
397 params=urllib.urlencode(params))
398 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
399 for build in content.get('builds', []):
400 builds[build['id']] = build
401 if 'next_cursor' in content:
402 params['start_cursor'] = content['next_cursor']
403 else:
404 break
405 return builds
406
407
408def print_tryjobs(options, builds):
409 """Prints nicely result of fetch_try_jobs."""
410 if not builds:
411 print 'No tryjobs scheduled'
412 return
413
414 # Make a copy, because we'll be modifying builds dictionary.
415 builds = builds.copy()
416 builder_names_cache = {}
417
418 def get_builder(b):
419 try:
420 return builder_names_cache[b['id']]
421 except KeyError:
422 try:
423 parameters = json.loads(b['parameters_json'])
424 name = parameters['builder_name']
425 except (ValueError, KeyError) as error:
426 print 'WARNING: failed to get builder name for build %s: %s' % (
427 b['id'], error)
428 name = None
429 builder_names_cache[b['id']] = name
430 return name
431
432 def get_bucket(b):
433 bucket = b['bucket']
434 if bucket.startswith('master.'):
435 return bucket[len('master.'):]
436 return bucket
437
438 if options.print_master:
439 name_fmt = '%%-%ds %%-%ds' % (
440 max(len(str(get_bucket(b))) for b in builds.itervalues()),
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % (get_bucket(b), get_builder(b))
444 else:
445 name_fmt = '%%-%ds' % (
446 max(len(str(get_builder(b))) for b in builds.itervalues()))
447 def get_name(b):
448 return name_fmt % get_builder(b)
449
450 def sort_key(b):
451 return b['status'], b.get('result'), get_name(b), b.get('url')
452
453 def pop(title, f, color=None, **kwargs):
454 """Pop matching builds from `builds` dict and print them."""
455
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000456 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 colorize = str
458 else:
459 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
460
461 result = []
462 for b in builds.values():
463 if all(b.get(k) == v for k, v in kwargs.iteritems()):
464 builds.pop(b['id'])
465 result.append(b)
466 if result:
467 print colorize(title)
468 for b in sorted(result, key=sort_key):
469 print ' ', colorize('\t'.join(map(str, f(b))))
470
471 total = len(builds)
472 pop(status='COMPLETED', result='SUCCESS',
473 title='Successes:', color=Fore.GREEN,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
476 title='Infra Failures:', color=Fore.MAGENTA,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
479 title='Failures:', color=Fore.RED,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='CANCELED',
482 title='Canceled:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 failure_reason='INVALID_BUILD_DEFINITION',
486 title='Wrong master/builder name:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 title='Other failures:',
490 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
491 pop(status='COMPLETED',
492 title='Other finished:',
493 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
494 pop(status='STARTED',
495 title='Started:', color=Fore.YELLOW,
496 f=lambda b: (get_name(b), b.get('url')))
497 pop(status='SCHEDULED',
498 title='Scheduled:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 # The last section is just in case buildbucket API changes OR there is a bug.
501 pop(title='Other:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 assert len(builds) == 0
504 print 'Total: %d tryjobs' % total
505
506
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000507def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
508 """Return the corresponding git ref if |base_url| together with |glob_spec|
509 matches the full |url|.
510
511 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
512 """
513 fetch_suburl, as_ref = glob_spec.split(':')
514 if allow_wildcards:
515 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
516 if glob_match:
517 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
518 # "branches/{472,597,648}/src:refs/remotes/svn/*".
519 branch_re = re.escape(base_url)
520 if glob_match.group(1):
521 branch_re += '/' + re.escape(glob_match.group(1))
522 wildcard = glob_match.group(2)
523 if wildcard == '*':
524 branch_re += '([^/]*)'
525 else:
526 # Escape and replace surrounding braces with parentheses and commas
527 # with pipe symbols.
528 wildcard = re.escape(wildcard)
529 wildcard = re.sub('^\\\\{', '(', wildcard)
530 wildcard = re.sub('\\\\,', '|', wildcard)
531 wildcard = re.sub('\\\\}$', ')', wildcard)
532 branch_re += wildcard
533 if glob_match.group(3):
534 branch_re += re.escape(glob_match.group(3))
535 match = re.match(branch_re, url)
536 if match:
537 return re.sub('\*$', match.group(1), as_ref)
538
539 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
540 if fetch_suburl:
541 full_url = base_url + '/' + fetch_suburl
542 else:
543 full_url = base_url
544 if full_url == url:
545 return as_ref
546 return None
547
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000548
iannucci@chromium.org79540052012-10-19 23:15:26 +0000549def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000550 """Prints statistics about the change to the user."""
551 # --no-ext-diff is broken in some versions of Git, so try to work around
552 # this by overriding the environment (but there is still a problem if the
553 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000555 if 'GIT_EXTERNAL_DIFF' in env:
556 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000557
558 if find_copies:
559 similarity_options = ['--find-copies-harder', '-l100000',
560 '-C%s' % similarity]
561 else:
562 similarity_options = ['-M%s' % similarity]
563
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000564 try:
565 stdout = sys.stdout.fileno()
566 except AttributeError:
567 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000569 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000570 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000571 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572
573
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000574class BuildbucketResponseException(Exception):
575 pass
576
577
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578class Settings(object):
579 def __init__(self):
580 self.default_server = None
581 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000582 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 self.is_git_svn = None
584 self.svn_branch = None
585 self.tree_status_url = None
586 self.viewvc_url = None
587 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000588 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000589 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000590 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000591 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000592 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000593 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594
595 def LazyUpdateIfNeeded(self):
596 """Updates the settings from a codereview.settings file, if available."""
597 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 # The only value that actually changes the behavior is
599 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000600 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 error_ok=True
602 ).strip().lower()
603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 LoadCodereviewSettingsFromFile(cr_settings_file)
607 self.updated = True
608
609 def GetDefaultServerUrl(self, error_ok=False):
610 if not self.default_server:
611 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000612 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000613 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 if error_ok:
615 return self.default_server
616 if not self.default_server:
617 error_message = ('Could not find settings file. You must configure '
618 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000619 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000620 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621 return self.default_server
622
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000623 @staticmethod
624 def GetRelativeRoot():
625 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 if self.root is None:
629 self.root = os.path.abspath(self.GetRelativeRoot())
630 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000632 def GetGitMirror(self, remote='origin'):
633 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000634 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000635 if not os.path.isdir(local_url):
636 return None
637 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
638 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
639 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
640 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
641 if mirror.exists():
642 return mirror
643 return None
644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 def GetIsGitSvn(self):
646 """Return true if this repo looks like it's using git-svn."""
647 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000648 if self.GetPendingRefPrefix():
649 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
650 self.is_git_svn = False
651 else:
652 # If you have any "svn-remote.*" config keys, we think you're using svn.
653 self.is_git_svn = RunGitWithCode(
654 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return self.is_git_svn
656
657 def GetSVNBranch(self):
658 if self.svn_branch is None:
659 if not self.GetIsGitSvn():
660 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
661
662 # Try to figure out which remote branch we're based on.
663 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000664 # 1) iterate through our branch history and find the svn URL.
665 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666
667 # regexp matching the git-svn line that contains the URL.
668 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
669
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 # We don't want to go through all of history, so read a line from the
671 # pipe at a time.
672 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000673 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000674 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
675 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000676 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 for line in proc.stdout:
678 match = git_svn_re.match(line)
679 if match:
680 url = match.group(1)
681 proc.stdout.close() # Cut pipe.
682 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 if url:
685 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
686 remotes = RunGit(['config', '--get-regexp',
687 r'^svn-remote\..*\.url']).splitlines()
688 for remote in remotes:
689 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 remote = match.group(1)
692 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000693 rewrite_root = RunGit(
694 ['config', 'svn-remote.%s.rewriteRoot' % remote],
695 error_ok=True).strip()
696 if rewrite_root:
697 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000698 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000699 ['config', 'svn-remote.%s.fetch' % remote],
700 error_ok=True).strip()
701 if fetch_spec:
702 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
703 if self.svn_branch:
704 break
705 branch_spec = RunGit(
706 ['config', 'svn-remote.%s.branches' % remote],
707 error_ok=True).strip()
708 if branch_spec:
709 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
710 if self.svn_branch:
711 break
712 tag_spec = RunGit(
713 ['config', 'svn-remote.%s.tags' % remote],
714 error_ok=True).strip()
715 if tag_spec:
716 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
717 if self.svn_branch:
718 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
720 if not self.svn_branch:
721 DieWithError('Can\'t guess svn branch -- try specifying it on the '
722 'command line')
723
724 return self.svn_branch
725
726 def GetTreeStatusUrl(self, error_ok=False):
727 if not self.tree_status_url:
728 error_message = ('You must configure your tree status URL by running '
729 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000730 self.tree_status_url = self._GetRietveldConfig(
731 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 return self.tree_status_url
733
734 def GetViewVCUrl(self):
735 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return self.viewvc_url
738
rmistry@google.com90752582014-01-14 21:04:50 +0000739 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000741
rmistry@google.com78948ed2015-07-08 23:09:57 +0000742 def GetIsSkipDependencyUpload(self, branch_name):
743 """Returns true if specified branch should skip dep uploads."""
744 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
745 error_ok=True)
746
rmistry@google.com5626a922015-02-26 14:03:30 +0000747 def GetRunPostUploadHook(self):
748 run_post_upload_hook = self._GetRietveldConfig(
749 'run-post-upload-hook', error_ok=True)
750 return run_post_upload_hook == "True"
751
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000752 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000753 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000755 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000756 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757
ukai@chromium.orge8077812012-02-03 03:41:46 +0000758 def GetIsGerrit(self):
759 """Return true if this repo is assosiated with gerrit code review system."""
760 if self.is_gerrit is None:
761 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
762 return self.is_gerrit
763
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000764 def GetSquashGerritUploads(self):
765 """Return true if uploads to Gerrit should be squashed by default."""
766 if self.squash_gerrit_uploads is None:
767 self.squash_gerrit_uploads = (
768 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
769 error_ok=True).strip() == 'true')
770 return self.squash_gerrit_uploads
771
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 def GetGitEditor(self):
773 """Return the editor specified in the git config, or None if none is."""
774 if self.git_editor is None:
775 self.git_editor = self._GetConfig('core.editor', error_ok=True)
776 return self.git_editor or None
777
thestig@chromium.org44202a22014-03-11 19:22:18 +0000778 def GetLintRegex(self):
779 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
780 DEFAULT_LINT_REGEX)
781
782 def GetLintIgnoreRegex(self):
783 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
784 DEFAULT_LINT_IGNORE_REGEX)
785
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000786 def GetProject(self):
787 if not self.project:
788 self.project = self._GetRietveldConfig('project', error_ok=True)
789 return self.project
790
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000791 def GetForceHttpsCommitUrl(self):
792 if not self.force_https_commit_url:
793 self.force_https_commit_url = self._GetRietveldConfig(
794 'force-https-commit-url', error_ok=True)
795 return self.force_https_commit_url
796
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000797 def GetPendingRefPrefix(self):
798 if not self.pending_ref_prefix:
799 self.pending_ref_prefix = self._GetRietveldConfig(
800 'pending-ref-prefix', error_ok=True)
801 return self.pending_ref_prefix
802
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000803 def _GetRietveldConfig(self, param, **kwargs):
804 return self._GetConfig('rietveld.' + param, **kwargs)
805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def _GetBranchConfig(self, branch_name, param, **kwargs):
807 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 def _GetConfig(self, param, **kwargs):
810 self.LazyUpdateIfNeeded()
811 return RunGit(['config', param], **kwargs).strip()
812
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814def ShortBranchName(branch):
815 """Convert a name like 'refs/heads/foo' to just 'foo'."""
816 return branch.replace('refs/heads/', '')
817
818
819class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000820 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000822 global settings
823 if not settings:
824 # Happens when git_cl.py is used as a utility library.
825 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 settings.GetDefaultServerUrl()
827 self.branchref = branchref
828 if self.branchref:
829 self.branch = ShortBranchName(self.branchref)
830 else:
831 self.branch = None
832 self.rietveld_server = None
833 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000834 self.lookedup_issue = False
835 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 self.has_description = False
837 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000838 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000840 self.cc = None
841 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000842 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000843 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000844 self._remote = None
845 self._rpc_server = None
846
847 @property
848 def auth_config(self):
849 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000850
851 def GetCCList(self):
852 """Return the users cc'd on this CL.
853
854 Return is a string suitable for passing to gcl with the --cc flag.
855 """
856 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000857 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000858 more_cc = ','.join(self.watchers)
859 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
860 return self.cc
861
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000862 def GetCCListWithoutDefault(self):
863 """Return the users cc'd on this CL excluding default ones."""
864 if self.cc is None:
865 self.cc = ','.join(self.watchers)
866 return self.cc
867
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000868 def SetWatchers(self, watchers):
869 """Set the list of email addresses that should be cc'd based on the changed
870 files in this CL.
871 """
872 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
874 def GetBranch(self):
875 """Returns the short branch name, e.g. 'master'."""
876 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000877 branchref = RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip()
879 if not branchref:
880 return None
881 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 self.branch = ShortBranchName(self.branchref)
883 return self.branch
884
885 def GetBranchRef(self):
886 """Returns the full branch name, e.g. 'refs/heads/master'."""
887 self.GetBranch() # Poke the lazy loader.
888 return self.branchref
889
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000890 @staticmethod
891 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000892 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 e.g. 'origin', 'refs/heads/master'
894 """
895 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
897 error_ok=True).strip()
898 if upstream_branch:
899 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
900 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000901 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
902 error_ok=True).strip()
903 if upstream_branch:
904 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000906 # Fall back on trying a git-svn upstream branch.
907 if settings.GetIsGitSvn():
908 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000910 # Else, try to guess the origin remote.
911 remote_branches = RunGit(['branch', '-r']).split()
912 if 'origin/master' in remote_branches:
913 # Fall back on origin/master if it exits.
914 remote = 'origin'
915 upstream_branch = 'refs/heads/master'
916 elif 'origin/trunk' in remote_branches:
917 # Fall back on origin/trunk if it exists. Generally a shared
918 # git-svn clone
919 remote = 'origin'
920 upstream_branch = 'refs/heads/trunk'
921 else:
922 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923Either pass complete "git diff"-style arguments, like
924 git cl upload origin/master
925or verify this branch is set up to track another (via the --track argument to
926"git checkout -b ...").""")
927
928 return remote, upstream_branch
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000931 upstream_branch = self.GetUpstreamBranch()
932 if not BranchExists(upstream_branch):
933 DieWithError('The upstream for the current branch (%s) does not exist '
934 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000935 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000936 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 def GetUpstreamBranch(self):
939 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000940 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000942 upstream_branch = upstream_branch.replace('refs/heads/',
943 'refs/remotes/%s/' % remote)
944 upstream_branch = upstream_branch.replace('refs/branch-heads/',
945 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.upstream_branch = upstream_branch
947 return self.upstream_branch
948
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000949 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000950 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000951 remote, branch = None, self.GetBranch()
952 seen_branches = set()
953 while branch not in seen_branches:
954 seen_branches.add(branch)
955 remote, branch = self.FetchUpstreamTuple(branch)
956 branch = ShortBranchName(branch)
957 if remote != '.' or branch.startswith('refs/remotes'):
958 break
959 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000960 remotes = RunGit(['remote'], error_ok=True).split()
961 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000962 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000963 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000965 logging.warning('Could not determine which remote this change is '
966 'associated with, so defaulting to "%s". This may '
967 'not be what you want. You may prevent this message '
968 'by running "git svn info" as documented here: %s',
969 self._remote,
970 GIT_INSTRUCTIONS_URL)
971 else:
972 logging.warn('Could not determine which remote this change is '
973 'associated with. You may prevent this message by '
974 'running "git svn info" as documented here: %s',
975 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000976 branch = 'HEAD'
977 if branch.startswith('refs/remotes'):
978 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000979 elif branch.startswith('refs/branch-heads/'):
980 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000981 else:
982 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000983 return self._remote
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 def GitSanityChecks(self, upstream_git_obj):
986 """Checks git repo status and ensures diff is from local commits."""
987
sbc@chromium.org79706062015-01-14 21:18:12 +0000988 if upstream_git_obj is None:
989 if self.GetBranch() is None:
990 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000991 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000992 else:
993 print >> sys.stderr, (
994 'ERROR: no upstream branch')
995 return False
996
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000997 # Verify the commit we're diffing against is in our current branch.
998 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
999 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1000 if upstream_sha != common_ancestor:
1001 print >> sys.stderr, (
1002 'ERROR: %s is not in the current branch. You may need to rebase '
1003 'your tracking branch' % upstream_sha)
1004 return False
1005
1006 # List the commits inside the diff, and verify they are all local.
1007 commits_in_diff = RunGit(
1008 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1009 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1010 remote_branch = remote_branch.strip()
1011 if code != 0:
1012 _, remote_branch = self.GetRemoteBranch()
1013
1014 commits_in_remote = RunGit(
1015 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1016
1017 common_commits = set(commits_in_diff) & set(commits_in_remote)
1018 if common_commits:
1019 print >> sys.stderr, (
1020 'ERROR: Your diff contains %d commits already in %s.\n'
1021 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1022 'the diff. If you are using a custom git flow, you can override'
1023 ' the reference used for this check with "git config '
1024 'gitcl.remotebranch <git-ref>".' % (
1025 len(common_commits), remote_branch, upstream_git_obj))
1026 return False
1027 return True
1028
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001029 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001030 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001031
1032 Returns None if it is not set.
1033 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001034 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1035 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001036
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001037 def GetGitSvnRemoteUrl(self):
1038 """Return the configured git-svn remote URL parsed from git svn info.
1039
1040 Returns None if it is not set.
1041 """
1042 # URL is dependent on the current directory.
1043 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1044 if data:
1045 keys = dict(line.split(': ', 1) for line in data.splitlines()
1046 if ': ' in line)
1047 return keys.get('URL', None)
1048 return None
1049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 def GetRemoteUrl(self):
1051 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1052
1053 Returns None if there is no remote.
1054 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001056 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1057
1058 # If URL is pointing to a local directory, it is probably a git cache.
1059 if os.path.isdir(url):
1060 url = RunGit(['config', 'remote.%s.url' % remote],
1061 error_ok=True,
1062 cwd=url).strip()
1063 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064
1065 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001066 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001067 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001069 self.issue = int(issue) or None if issue else None
1070 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 return self.issue
1072
1073 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001074 if not self.rietveld_server:
1075 # If we're on a branch then get the server potentially associated
1076 # with that branch.
1077 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001078 rietveld_server_config = self._RietveldServer()
1079 if rietveld_server_config:
1080 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1081 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001082 if not self.rietveld_server:
1083 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 return self.rietveld_server
1085
1086 def GetIssueURL(self):
1087 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001088 if not self.GetIssue():
1089 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1091
1092 def GetDescription(self, pretty=False):
1093 if not self.has_description:
1094 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001095 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001096 try:
1097 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001098 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001099 if e.code == 404:
1100 DieWithError(
1101 ('\nWhile fetching the description for issue %d, received a '
1102 '404 (not found)\n'
1103 'error. It is likely that you deleted this '
1104 'issue on the server. If this is the\n'
1105 'case, please run\n\n'
1106 ' git cl issue 0\n\n'
1107 'to clear the association with the deleted issue. Then run '
1108 'this command again.') % issue)
1109 else:
1110 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001111 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001112 except urllib2.URLError as e:
1113 print >> sys.stderr, (
1114 'Warning: Failed to retrieve CL description due to network '
1115 'failure.')
1116 self.description = ''
1117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.has_description = True
1119 if pretty:
1120 wrapper = textwrap.TextWrapper()
1121 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1122 return wrapper.fill(self.description)
1123 return self.description
1124
1125 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001126 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 patchset = RunGit(['config', self._PatchsetSetting()],
1129 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = int(patchset) or None if patchset else None
1131 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 return self.patchset
1133
1134 def SetPatchset(self, patchset):
1135 """Set this branch's patchset. If patchset=0, clears the patchset."""
1136 if patchset:
1137 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001138 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 else:
1140 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001141 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001142 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 def GetMostRecentPatchset(self):
1145 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001146
1147 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001148 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001149 '/download/issue%s_%s.diff' % (issue, patchset))
1150
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 def GetIssueProperties(self):
1152 if self._props is None:
1153 issue = self.GetIssue()
1154 if not issue:
1155 self._props = {}
1156 else:
1157 self._props = self.RpcServer().get_issue_properties(issue, True)
1158 return self._props
1159
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001160 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001161 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001162
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001163 def AddComment(self, message):
1164 return self.RpcServer().add_comment(self.GetIssue(), message)
1165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 def SetIssue(self, issue):
1167 """Set this branch's issue. If issue=0, clears the issue."""
1168 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001169 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 RunGit(['config', self._IssueSetting(), str(issue)])
1171 if self.rietveld_server:
1172 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1173 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001174 current_issue = self.GetIssue()
1175 if current_issue:
1176 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = None
1178 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001180 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001181 if not self.GitSanityChecks(upstream_branch):
1182 DieWithError('\nGit sanity check failure')
1183
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001184 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001185 if not root:
1186 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001187 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188
1189 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001190 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001191 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001192 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001193 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001194 except subprocess2.CalledProcessError:
1195 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001196 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001197 'This branch probably doesn\'t exist anymore. To reset the\n'
1198 'tracking branch, please run\n'
1199 ' git branch --set-upstream %s trunk\n'
1200 'replacing trunk with origin/master or the relevant branch') %
1201 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001202
maruel@chromium.org52424302012-08-29 15:14:30 +00001203 issue = self.GetIssue()
1204 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 if issue:
1206 description = self.GetDescription()
1207 else:
1208 # If the change was never uploaded, use the log messages of all commits
1209 # up to the branch point, as git cl upload will prefill the description
1210 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001211 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1212 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001213
1214 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001215 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001216 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001217 name,
1218 description,
1219 absroot,
1220 files,
1221 issue,
1222 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001223 author,
1224 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001225
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001226 def GetStatus(self):
1227 """Apply a rough heuristic to give a simple summary of an issue's review
1228 or CQ status, assuming adherence to a common workflow.
1229
1230 Returns None if no issue for this branch, or one of the following keywords:
1231 * 'error' - error from review tool (including deleted issues)
1232 * 'unsent' - not sent for review
1233 * 'waiting' - waiting for review
1234 * 'reply' - waiting for owner to reply to review
1235 * 'lgtm' - LGTM from at least one approved reviewer
1236 * 'commit' - in the commit queue
1237 * 'closed' - closed
1238 """
1239 if not self.GetIssue():
1240 return None
1241
1242 try:
1243 props = self.GetIssueProperties()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if props.get('closed'):
1248 # Issue is closed.
1249 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001250 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001251 # Issue is in the commit queue.
1252 return 'commit'
1253
1254 try:
1255 reviewers = self.GetApprovingReviewers()
1256 except urllib2.HTTPError:
1257 return 'error'
1258
1259 if reviewers:
1260 # Was LGTM'ed.
1261 return 'lgtm'
1262
1263 messages = props.get('messages') or []
1264
1265 if not messages:
1266 # No message was sent.
1267 return 'unsent'
1268 if messages[-1]['sender'] != props.get('owner_email'):
1269 # Non-LGTM reply from non-owner
1270 return 'reply'
1271 return 'waiting'
1272
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001273 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001274 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001275
1276 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001277 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001278 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001279 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001280 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281 except presubmit_support.PresubmitFailure, e:
1282 DieWithError(
1283 ('%s\nMaybe your depot_tools is out of date?\n'
1284 'If all fails, contact maruel@') % e)
1285
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self.RpcServer().update_description(
1289 self.GetIssue(), self.description)
1290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001292 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001293 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001295 def SetFlag(self, flag, value):
1296 """Patchset must match."""
1297 if not self.GetPatchset():
1298 DieWithError('The patchset needs to match. Send another patchset.')
1299 try:
1300 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001301 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001302 except urllib2.HTTPError, e:
1303 if e.code == 404:
1304 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1305 if e.code == 403:
1306 DieWithError(
1307 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1308 'match?') % (self.GetIssue(), self.GetPatchset()))
1309 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001311 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 """Returns an upload.RpcServer() to access this review's rietveld instance.
1313 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001314 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001315 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001316 self.GetRietveldServer(),
1317 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001318 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
1320 def _IssueSetting(self):
1321 """Return the git setting that stores this change's issue."""
1322 return 'branch.%s.rietveldissue' % self.GetBranch()
1323
1324 def _PatchsetSetting(self):
1325 """Return the git setting that stores this change's most recent patchset."""
1326 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1327
1328 def _RietveldServer(self):
1329 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001330 branch = self.GetBranch()
1331 if branch:
1332 return 'branch.%s.rietveldserver' % branch
1333 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
1335
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001336class ChangeDescription(object):
1337 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001338 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001339 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001340
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001341 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001342 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001343
agable@chromium.org42c20792013-09-12 17:34:49 +00001344 @property # www.logilab.org/ticket/89786
1345 def description(self): # pylint: disable=E0202
1346 return '\n'.join(self._description_lines)
1347
1348 def set_description(self, desc):
1349 if isinstance(desc, basestring):
1350 lines = desc.splitlines()
1351 else:
1352 lines = [line.rstrip() for line in desc]
1353 while lines and not lines[0]:
1354 lines.pop(0)
1355 while lines and not lines[-1]:
1356 lines.pop(-1)
1357 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001358
piman@chromium.org336f9122014-09-04 02:16:55 +00001359 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001360 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001361 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001362 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001363 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001364 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365
agable@chromium.org42c20792013-09-12 17:34:49 +00001366 # Get the set of R= and TBR= lines and remove them from the desciption.
1367 regexp = re.compile(self.R_LINE)
1368 matches = [regexp.match(line) for line in self._description_lines]
1369 new_desc = [l for i, l in enumerate(self._description_lines)
1370 if not matches[i]]
1371 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 # Construct new unified R= and TBR= lines.
1374 r_names = []
1375 tbr_names = []
1376 for match in matches:
1377 if not match:
1378 continue
1379 people = cleanup_list([match.group(2).strip()])
1380 if match.group(1) == 'TBR':
1381 tbr_names.extend(people)
1382 else:
1383 r_names.extend(people)
1384 for name in r_names:
1385 if name not in reviewers:
1386 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001387 if add_owners_tbr:
1388 owners_db = owners.Database(change.RepositoryRoot(),
1389 fopen=file, os_path=os.path, glob=glob.glob)
1390 all_reviewers = set(tbr_names + reviewers)
1391 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1392 all_reviewers)
1393 tbr_names.extend(owners_db.reviewers_for(missing_files,
1394 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001395 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1396 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1397
1398 # Put the new lines in the description where the old first R= line was.
1399 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1400 if 0 <= line_loc < len(self._description_lines):
1401 if new_tbr_line:
1402 self._description_lines.insert(line_loc, new_tbr_line)
1403 if new_r_line:
1404 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001405 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001406 if new_r_line:
1407 self.append_footer(new_r_line)
1408 if new_tbr_line:
1409 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001410
1411 def prompt(self):
1412 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001413 self.set_description([
1414 '# Enter a description of the change.',
1415 '# This will be displayed on the codereview site.',
1416 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001417 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001418 '--------------------',
1419 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001420
agable@chromium.org42c20792013-09-12 17:34:49 +00001421 regexp = re.compile(self.BUG_LINE)
1422 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001423 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001425 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001426 if not content:
1427 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001428 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429
1430 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001431 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1432 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001433 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001434 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001435
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001436 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001437 if self._description_lines:
1438 # Add an empty line if either the last line or the new line isn't a tag.
1439 last_line = self._description_lines[-1]
1440 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1441 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1442 self._description_lines.append('')
1443 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001444
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001445 def get_reviewers(self):
1446 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1448 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001449 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001450
1451
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001452def get_approving_reviewers(props):
1453 """Retrieves the reviewers that approved a CL from the issue properties with
1454 messages.
1455
1456 Note that the list may contain reviewers that are not committer, thus are not
1457 considered by the CQ.
1458 """
1459 return sorted(
1460 set(
1461 message['sender']
1462 for message in props['messages']
1463 if message['approval'] and message['sender'] in props['reviewers']
1464 )
1465 )
1466
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468def FindCodereviewSettingsFile(filename='codereview.settings'):
1469 """Finds the given file starting in the cwd and going up.
1470
1471 Only looks up to the top of the repository unless an
1472 'inherit-review-settings-ok' file exists in the root of the repository.
1473 """
1474 inherit_ok_file = 'inherit-review-settings-ok'
1475 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001476 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1478 root = '/'
1479 while True:
1480 if filename in os.listdir(cwd):
1481 if os.path.isfile(os.path.join(cwd, filename)):
1482 return open(os.path.join(cwd, filename))
1483 if cwd == root:
1484 break
1485 cwd = os.path.dirname(cwd)
1486
1487
1488def LoadCodereviewSettingsFromFile(fileobj):
1489 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001490 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 def SetProperty(name, setting, unset_error_ok=False):
1493 fullname = 'rietveld.' + name
1494 if setting in keyvals:
1495 RunGit(['config', fullname, keyvals[setting]])
1496 else:
1497 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1498
1499 SetProperty('server', 'CODE_REVIEW_SERVER')
1500 # Only server setting is required. Other settings can be absent.
1501 # In that case, we ignore errors raised during option deletion attempt.
1502 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001503 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1505 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001506 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001507 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001508 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1509 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001510 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001511 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001512 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001513 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1514 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001516 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001518
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001519 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1520 RunGit(['config', 'gerrit.squash-uploads',
1521 keyvals['GERRIT_SQUASH_UPLOADS']])
1522
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001523 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1524 #should be of the form
1525 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1526 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1527 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1528 keyvals['ORIGIN_URL_CONFIG']])
1529
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001531def urlretrieve(source, destination):
1532 """urllib is broken for SSL connections via a proxy therefore we
1533 can't use urllib.urlretrieve()."""
1534 with open(destination, 'w') as f:
1535 f.write(urllib2.urlopen(source).read())
1536
1537
ukai@chromium.org712d6102013-11-27 00:52:58 +00001538def hasSheBang(fname):
1539 """Checks fname is a #! script."""
1540 with open(fname) as f:
1541 return f.read(2).startswith('#!')
1542
1543
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001544def DownloadGerritHook(force):
1545 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001546
1547 Args:
1548 force: True to update hooks. False to install hooks if not present.
1549 """
1550 if not settings.GetIsGerrit():
1551 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001552 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001553 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1554 if not os.access(dst, os.X_OK):
1555 if os.path.exists(dst):
1556 if not force:
1557 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001558 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001559 print(
1560 'WARNING: installing Gerrit commit-msg hook.\n'
1561 ' This behavior of git cl will soon be disabled.\n'
1562 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001563 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001564 if not hasSheBang(dst):
1565 DieWithError('Not a script: %s\n'
1566 'You need to download from\n%s\n'
1567 'into .git/hooks/commit-msg and '
1568 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001569 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1570 except Exception:
1571 if os.path.exists(dst):
1572 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001573 DieWithError('\nFailed to download hooks.\n'
1574 'You need to download from\n%s\n'
1575 'into .git/hooks/commit-msg and '
1576 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001577
1578
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001579
1580def GetRietveldCodereviewSettingsInteractively():
1581 """Prompt the user for settings."""
1582 server = settings.GetDefaultServerUrl(error_ok=True)
1583 prompt = 'Rietveld server (host[:port])'
1584 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1585 newserver = ask_for_data(prompt + ':')
1586 if not server and not newserver:
1587 newserver = DEFAULT_SERVER
1588 if newserver:
1589 newserver = gclient_utils.UpgradeToHttps(newserver)
1590 if newserver != server:
1591 RunGit(['config', 'rietveld.server', newserver])
1592
1593 def SetProperty(initial, caption, name, is_url):
1594 prompt = caption
1595 if initial:
1596 prompt += ' ("x" to clear) [%s]' % initial
1597 new_val = ask_for_data(prompt + ':')
1598 if new_val == 'x':
1599 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1600 elif new_val:
1601 if is_url:
1602 new_val = gclient_utils.UpgradeToHttps(new_val)
1603 if new_val != initial:
1604 RunGit(['config', 'rietveld.' + name, new_val])
1605
1606 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1607 SetProperty(settings.GetDefaultPrivateFlag(),
1608 'Private flag (rietveld only)', 'private', False)
1609 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1610 'tree-status-url', False)
1611 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1612 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1613 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1614 'run-post-upload-hook', False)
1615
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001616@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001618 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001620 print('WARNING: git cl config works for Rietveld only.\n'
1621 'For Gerrit, see http://crbug.com/579160.')
1622 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001623 parser.add_option('--activate-update', action='store_true',
1624 help='activate auto-updating [rietveld] section in '
1625 '.git/config')
1626 parser.add_option('--deactivate-update', action='store_true',
1627 help='deactivate auto-updating [rietveld] section in '
1628 '.git/config')
1629 options, args = parser.parse_args(args)
1630
1631 if options.deactivate_update:
1632 RunGit(['config', 'rietveld.autoupdate', 'false'])
1633 return
1634
1635 if options.activate_update:
1636 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1637 return
1638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001640 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 return 0
1642
1643 url = args[0]
1644 if not url.endswith('codereview.settings'):
1645 url = os.path.join(url, 'codereview.settings')
1646
1647 # Load code review settings and download hooks (if available).
1648 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1649 return 0
1650
1651
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001652def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001653 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001654 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1655 branch = ShortBranchName(branchref)
1656 _, args = parser.parse_args(args)
1657 if not args:
1658 print("Current base-url:")
1659 return RunGit(['config', 'branch.%s.base-url' % branch],
1660 error_ok=False).strip()
1661 else:
1662 print("Setting base-url to %s" % args[0])
1663 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1664 error_ok=False).strip()
1665
1666
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001667def color_for_status(status):
1668 """Maps a Changelist status to color, for CMDstatus and other tools."""
1669 return {
1670 'unsent': Fore.RED,
1671 'waiting': Fore.BLUE,
1672 'reply': Fore.YELLOW,
1673 'lgtm': Fore.GREEN,
1674 'commit': Fore.MAGENTA,
1675 'closed': Fore.CYAN,
1676 'error': Fore.WHITE,
1677 }.get(status, Fore.WHITE)
1678
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001679def fetch_cl_status(branch, auth_config=None):
1680 """Fetches information for an issue and returns (branch, issue, status)."""
1681 cl = Changelist(branchref=branch, auth_config=auth_config)
1682 url = cl.GetIssueURL()
1683 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001684
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001685 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001686 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001687 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001688
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001689 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001690
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001691def get_cl_statuses(
1692 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001693 """Returns a blocking iterable of (branch, issue, color) for given branches.
1694
1695 If fine_grained is true, this will fetch CL statuses from the server.
1696 Otherwise, simply indicate if there's a matching url for the given branches.
1697
1698 If max_processes is specified, it is used as the maximum number of processes
1699 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1700 spawned.
1701 """
1702 # Silence upload.py otherwise it becomes unwieldly.
1703 upload.verbosity = 0
1704
1705 if fine_grained:
1706 # Process one branch synchronously to work through authentication, then
1707 # spawn processes to process all the other branches in parallel.
1708 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001709 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1710 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001711
1712 branches_to_fetch = branches[1:]
1713 pool = ThreadPool(
1714 min(max_processes, len(branches_to_fetch))
1715 if max_processes is not None
1716 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001717 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001718 yield x
1719 else:
1720 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1721 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001722 cl = Changelist(branchref=b, auth_config=auth_config)
1723 url = cl.GetIssueURL()
1724 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001725
rmistry@google.com2dd99862015-06-22 12:22:18 +00001726
1727def upload_branch_deps(cl, args):
1728 """Uploads CLs of local branches that are dependents of the current branch.
1729
1730 If the local branch dependency tree looks like:
1731 test1 -> test2.1 -> test3.1
1732 -> test3.2
1733 -> test2.2 -> test3.3
1734
1735 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1736 run on the dependent branches in this order:
1737 test2.1, test3.1, test3.2, test2.2, test3.3
1738
1739 Note: This function does not rebase your local dependent branches. Use it when
1740 you make a change to the parent branch that will not conflict with its
1741 dependent branches, and you would like their dependencies updated in
1742 Rietveld.
1743 """
1744 if git_common.is_dirty_git_tree('upload-branch-deps'):
1745 return 1
1746
1747 root_branch = cl.GetBranch()
1748 if root_branch is None:
1749 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1750 'Get on a branch!')
1751 if not cl.GetIssue() or not cl.GetPatchset():
1752 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1753 'patchset dependencies without an uploaded CL.')
1754
1755 branches = RunGit(['for-each-ref',
1756 '--format=%(refname:short) %(upstream:short)',
1757 'refs/heads'])
1758 if not branches:
1759 print('No local branches found.')
1760 return 0
1761
1762 # Create a dictionary of all local branches to the branches that are dependent
1763 # on it.
1764 tracked_to_dependents = collections.defaultdict(list)
1765 for b in branches.splitlines():
1766 tokens = b.split()
1767 if len(tokens) == 2:
1768 branch_name, tracked = tokens
1769 tracked_to_dependents[tracked].append(branch_name)
1770
1771 print
1772 print 'The dependent local branches of %s are:' % root_branch
1773 dependents = []
1774 def traverse_dependents_preorder(branch, padding=''):
1775 dependents_to_process = tracked_to_dependents.get(branch, [])
1776 padding += ' '
1777 for dependent in dependents_to_process:
1778 print '%s%s' % (padding, dependent)
1779 dependents.append(dependent)
1780 traverse_dependents_preorder(dependent, padding)
1781 traverse_dependents_preorder(root_branch)
1782 print
1783
1784 if not dependents:
1785 print 'There are no dependent local branches for %s' % root_branch
1786 return 0
1787
1788 print ('This command will checkout all dependent branches and run '
1789 '"git cl upload".')
1790 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1791
andybons@chromium.org962f9462016-02-03 20:00:42 +00001792 # Add a default patchset title to all upload calls in Rietveld.
1793 if not settings.GetIsGerrit():
1794 args.extend(['-t', 'Updated patchset dependency'])
1795
rmistry@google.com2dd99862015-06-22 12:22:18 +00001796 # Record all dependents that failed to upload.
1797 failures = {}
1798 # Go through all dependents, checkout the branch and upload.
1799 try:
1800 for dependent_branch in dependents:
1801 print
1802 print '--------------------------------------'
1803 print 'Running "git cl upload" from %s:' % dependent_branch
1804 RunGit(['checkout', '-q', dependent_branch])
1805 print
1806 try:
1807 if CMDupload(OptionParser(), args) != 0:
1808 print 'Upload failed for %s!' % dependent_branch
1809 failures[dependent_branch] = 1
1810 except: # pylint: disable=W0702
1811 failures[dependent_branch] = 1
1812 print
1813 finally:
1814 # Swap back to the original root branch.
1815 RunGit(['checkout', '-q', root_branch])
1816
1817 print
1818 print 'Upload complete for dependent branches!'
1819 for dependent_branch in dependents:
1820 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1821 print ' %s : %s' % (dependent_branch, upload_status)
1822 print
1823
1824 return 0
1825
1826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001828 """Show status of changelists.
1829
1830 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001831 - Red not sent for review or broken
1832 - Blue waiting for review
1833 - Yellow waiting for you to reply to review
1834 - Green LGTM'ed
1835 - Magenta in the commit queue
1836 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001837
1838 Also see 'git cl comments'.
1839 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 parser.add_option('--field',
1841 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001842 parser.add_option('-f', '--fast', action='store_true',
1843 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001844 parser.add_option(
1845 '-j', '--maxjobs', action='store', type=int,
1846 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001847
1848 auth.add_auth_options(parser)
1849 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001850 if args:
1851 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001852 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001855 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856 if options.field.startswith('desc'):
1857 print cl.GetDescription()
1858 elif options.field == 'id':
1859 issueid = cl.GetIssue()
1860 if issueid:
1861 print issueid
1862 elif options.field == 'patch':
1863 patchset = cl.GetPatchset()
1864 if patchset:
1865 print patchset
1866 elif options.field == 'url':
1867 url = cl.GetIssueURL()
1868 if url:
1869 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001870 return 0
1871
1872 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1873 if not branches:
1874 print('No local branch found.')
1875 return 0
1876
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001877 changes = (
1878 Changelist(branchref=b, auth_config=auth_config)
1879 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001880 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001881 alignment = max(5, max(len(b) for b in branches))
1882 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001883 output = get_cl_statuses(branches,
1884 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001885 max_processes=options.maxjobs,
1886 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001887
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001888 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001889 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001890 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001891 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001892 b, i, status = output.next()
1893 branch_statuses[b] = (i, status)
1894 issue_url, status = branch_statuses.pop(branch)
1895 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001896 reset = Fore.RESET
1897 if not sys.stdout.isatty():
1898 color = ''
1899 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001900 status_str = '(%s)' % status if status else ''
1901 print ' %*s : %s%s %s%s' % (
1902 alignment, ShortBranchName(branch), color, issue_url, status_str,
1903 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001904
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001905 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001906 print
1907 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001908 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001909 if not cl.GetIssue():
1910 print 'No issue assigned.'
1911 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001912 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001913 if not options.fast:
1914 print 'Issue description:'
1915 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916 return 0
1917
1918
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001919def colorize_CMDstatus_doc():
1920 """To be called once in main() to add colors to git cl status help."""
1921 colors = [i for i in dir(Fore) if i[0].isupper()]
1922
1923 def colorize_line(line):
1924 for color in colors:
1925 if color in line.upper():
1926 # Extract whitespaces first and the leading '-'.
1927 indent = len(line) - len(line.lstrip(' ')) + 1
1928 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1929 return line
1930
1931 lines = CMDstatus.__doc__.splitlines()
1932 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1933
1934
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001935@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001937 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938
1939 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001940 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001941 parser.add_option('-r', '--reverse', action='store_true',
1942 help='Lookup the branch(es) for the specified issues. If '
1943 'no issues are specified, all branches with mapped '
1944 'issues will be listed.')
1945 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946
dnj@chromium.org406c4402015-03-03 17:22:28 +00001947 if options.reverse:
1948 branches = RunGit(['for-each-ref', 'refs/heads',
1949 '--format=%(refname:short)']).splitlines()
1950
1951 # Reverse issue lookup.
1952 issue_branch_map = {}
1953 for branch in branches:
1954 cl = Changelist(branchref=branch)
1955 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1956 if not args:
1957 args = sorted(issue_branch_map.iterkeys())
1958 for issue in args:
1959 if not issue:
1960 continue
1961 print 'Branch for issue number %s: %s' % (
1962 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1963 else:
1964 cl = Changelist()
1965 if len(args) > 0:
1966 try:
1967 issue = int(args[0])
1968 except ValueError:
1969 DieWithError('Pass a number to set the issue or none to list it.\n'
1970 'Maybe you want to run git cl status?')
1971 cl.SetIssue(issue)
1972 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 return 0
1974
1975
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001976def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001977 """Shows or posts review comments for any changelist."""
1978 parser.add_option('-a', '--add-comment', dest='comment',
1979 help='comment to add to an issue')
1980 parser.add_option('-i', dest='issue',
1981 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001982 parser.add_option('-j', '--json-file',
1983 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001984 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001985 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001986 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001987
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001988 issue = None
1989 if options.issue:
1990 try:
1991 issue = int(options.issue)
1992 except ValueError:
1993 DieWithError('A review issue id is expected to be a number')
1994
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001995 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001996
1997 if options.comment:
1998 cl.AddComment(options.comment)
1999 return 0
2000
2001 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002002 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002003 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002004 summary.append({
2005 'date': message['date'],
2006 'lgtm': False,
2007 'message': message['text'],
2008 'not_lgtm': False,
2009 'sender': message['sender'],
2010 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002011 if message['disapproval']:
2012 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002013 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002014 elif message['approval']:
2015 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002016 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002017 elif message['sender'] == data['owner_email']:
2018 color = Fore.MAGENTA
2019 else:
2020 color = Fore.BLUE
2021 print '\n%s%s %s%s' % (
2022 color, message['date'].split('.', 1)[0], message['sender'],
2023 Fore.RESET)
2024 if message['text'].strip():
2025 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002026 if options.json_file:
2027 with open(options.json_file, 'wb') as f:
2028 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002029 return 0
2030
2031
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002032def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002033 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002034 parser.add_option('-d', '--display', action='store_true',
2035 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002036 auth.add_auth_options(parser)
2037 options, _ = parser.parse_args(args)
2038 auth_config = auth.extract_auth_config_from_options(options)
2039 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002040 if not cl.GetIssue():
2041 DieWithError('This branch has no associated changelist.')
2042 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002043 if options.display:
2044 print description.description
2045 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002046 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002047 if cl.GetDescription() != description.description:
2048 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002049 return 0
2050
2051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002052def CreateDescriptionFromLog(args):
2053 """Pulls out the commit log to use as a base for the CL description."""
2054 log_args = []
2055 if len(args) == 1 and not args[0].endswith('.'):
2056 log_args = [args[0] + '..']
2057 elif len(args) == 1 and args[0].endswith('...'):
2058 log_args = [args[0][:-1]]
2059 elif len(args) == 2:
2060 log_args = [args[0] + '..' + args[1]]
2061 else:
2062 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002063 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064
2065
thestig@chromium.org44202a22014-03-11 19:22:18 +00002066def CMDlint(parser, args):
2067 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002068 parser.add_option('--filter', action='append', metavar='-x,+y',
2069 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002070 auth.add_auth_options(parser)
2071 options, args = parser.parse_args(args)
2072 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002073
2074 # Access to a protected member _XX of a client class
2075 # pylint: disable=W0212
2076 try:
2077 import cpplint
2078 import cpplint_chromium
2079 except ImportError:
2080 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2081 return 1
2082
2083 # Change the current working directory before calling lint so that it
2084 # shows the correct base.
2085 previous_cwd = os.getcwd()
2086 os.chdir(settings.GetRoot())
2087 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002088 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002089 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2090 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002091 if not files:
2092 print "Cannot lint an empty CL"
2093 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002094
2095 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002096 command = args + files
2097 if options.filter:
2098 command = ['--filter=' + ','.join(options.filter)] + command
2099 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002100
2101 white_regex = re.compile(settings.GetLintRegex())
2102 black_regex = re.compile(settings.GetLintIgnoreRegex())
2103 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2104 for filename in filenames:
2105 if white_regex.match(filename):
2106 if black_regex.match(filename):
2107 print "Ignoring file %s" % filename
2108 else:
2109 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2110 extra_check_functions)
2111 else:
2112 print "Skipping file %s" % filename
2113 finally:
2114 os.chdir(previous_cwd)
2115 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2116 if cpplint._cpplint_state.error_count != 0:
2117 return 1
2118 return 0
2119
2120
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002122 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002123 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002125 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002126 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002127 auth.add_auth_options(parser)
2128 options, args = parser.parse_args(args)
2129 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130
sbc@chromium.org71437c02015-04-09 19:29:40 +00002131 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002132 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 return 1
2134
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002135 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 if args:
2137 base_branch = args[0]
2138 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002139 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002140 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002142 cl.RunHook(
2143 committing=not options.upload,
2144 may_prompt=False,
2145 verbose=options.verbose,
2146 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002147 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002148
2149
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002150def AddChangeIdToCommitMessage(options, args):
2151 """Re-commits using the current message, assumes the commit hook is in
2152 place.
2153 """
2154 log_desc = options.message or CreateDescriptionFromLog(args)
2155 git_command = ['commit', '--amend', '-m', log_desc]
2156 RunGit(git_command)
2157 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002158 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002159 print 'git-cl: Added Change-Id to commit message.'
2160 else:
2161 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2162
2163
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002164def GenerateGerritChangeId(message):
2165 """Returns Ixxxxxx...xxx change id.
2166
2167 Works the same way as
2168 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2169 but can be called on demand on all platforms.
2170
2171 The basic idea is to generate git hash of a state of the tree, original commit
2172 message, author/committer info and timestamps.
2173 """
2174 lines = []
2175 tree_hash = RunGitSilent(['write-tree'])
2176 lines.append('tree %s' % tree_hash.strip())
2177 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2178 if code == 0:
2179 lines.append('parent %s' % parent.strip())
2180 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2181 lines.append('author %s' % author.strip())
2182 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2183 lines.append('committer %s' % committer.strip())
2184 lines.append('')
2185 # Note: Gerrit's commit-hook actually cleans message of some lines and
2186 # whitespace. This code is not doing this, but it clearly won't decrease
2187 # entropy.
2188 lines.append(message)
2189 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2190 stdin='\n'.join(lines))
2191 return 'I%s' % change_hash.strip()
2192
2193
piman@chromium.org336f9122014-09-04 02:16:55 +00002194def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002195 """upload the current branch to gerrit."""
2196 # We assume the remote called "origin" is the one we want.
2197 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002198 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002199
2200 remote, remote_branch = cl.GetRemoteBranch()
2201 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2202 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002204 change_desc = ChangeDescription(
2205 options.message or CreateDescriptionFromLog(args))
2206 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002207 print "\nDescription is empty. Aborting..."
2208 return 1
2209
2210 if options.title:
2211 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002213
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002214 if options.squash:
2215 # Try to get the message from a previous upload.
2216 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002217 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002218 if not message:
2219 if not options.force:
2220 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002221 if not change_desc.description:
2222 print "Description is empty; aborting."
2223 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002224 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002225 change_ids = git_footers.get_footer_change_id(message)
2226 if len(change_ids) > 1:
2227 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2228 if not change_ids:
2229 message = git_footers.add_footer_change_id(
2230 message, GenerateGerritChangeId(message))
2231 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002232
2233 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2234 if remote is '.':
2235 # If our upstream branch is local, we base our squashed commit on its
2236 # squashed version.
2237 parent = ('refs/heads/git_cl_uploads/' +
2238 scm.GIT.ShortBranchName(upstream_branch))
2239
2240 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2241 # will create additional CLs when uploading.
2242 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2243 RunGitSilent(['rev-parse', parent + ':'])):
2244 print 'Upload upstream branch ' + upstream_branch + ' first.'
2245 return 1
2246 else:
2247 parent = cl.GetCommonAncestorWithUpstream()
2248
2249 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2250 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2251 '-m', message]).strip()
2252 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002253 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002254 DownloadGerritHook(False)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002255 AddChangeIdToCommitMessage(options, args)
2256 ref_to_push = 'HEAD'
2257 parent = '%s/%s' % (gerrit_remote, branch)
2258
2259 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2260 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002261 if len(commits) > 1:
2262 print('WARNING: This will upload %d commits. Run the following command '
2263 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002264 print('git log %s..%s' % (parent, ref_to_push))
2265 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002266 'commit.')
2267 ask_for_data('About to upload; enter to confirm.')
2268
piman@chromium.org336f9122014-09-04 02:16:55 +00002269 if options.reviewers or options.tbr_owners:
2270 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002271
ukai@chromium.orge8077812012-02-03 03:41:46 +00002272 receive_options = []
2273 cc = cl.GetCCList().split(',')
2274 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002275 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002276 cc = filter(None, cc)
2277 if cc:
2278 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002279 if change_desc.get_reviewers():
2280 receive_options.extend(
2281 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282
ukai@chromium.orge8077812012-02-03 03:41:46 +00002283 git_command = ['push']
2284 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002285 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002286 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002287 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002288 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002289
2290 if options.squash:
2291 head = RunGit(['rev-parse', 'HEAD']).strip()
2292 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2293
ukai@chromium.orge8077812012-02-03 03:41:46 +00002294 # TODO(ukai): parse Change-Id: and set issue number?
2295 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002297
wittman@chromium.org455dc922015-01-26 20:15:50 +00002298def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2299 """Computes the remote branch ref to use for the CL.
2300
2301 Args:
2302 remote (str): The git remote for the CL.
2303 remote_branch (str): The git remote branch for the CL.
2304 target_branch (str): The target branch specified by the user.
2305 pending_prefix (str): The pending prefix from the settings.
2306 """
2307 if not (remote and remote_branch):
2308 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002309
wittman@chromium.org455dc922015-01-26 20:15:50 +00002310 if target_branch:
2311 # Cannonicalize branch references to the equivalent local full symbolic
2312 # refs, which are then translated into the remote full symbolic refs
2313 # below.
2314 if '/' not in target_branch:
2315 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2316 else:
2317 prefix_replacements = (
2318 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2319 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2320 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2321 )
2322 match = None
2323 for regex, replacement in prefix_replacements:
2324 match = re.search(regex, target_branch)
2325 if match:
2326 remote_branch = target_branch.replace(match.group(0), replacement)
2327 break
2328 if not match:
2329 # This is a branch path but not one we recognize; use as-is.
2330 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002331 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2332 # Handle the refs that need to land in different refs.
2333 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002334
wittman@chromium.org455dc922015-01-26 20:15:50 +00002335 # Create the true path to the remote branch.
2336 # Does the following translation:
2337 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2338 # * refs/remotes/origin/master -> refs/heads/master
2339 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2340 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2341 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2342 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2343 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2344 'refs/heads/')
2345 elif remote_branch.startswith('refs/remotes/branch-heads'):
2346 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2347 # If a pending prefix exists then replace refs/ with it.
2348 if pending_prefix:
2349 remote_branch = remote_branch.replace('refs/', pending_prefix)
2350 return remote_branch
2351
2352
piman@chromium.org336f9122014-09-04 02:16:55 +00002353def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002354 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2356 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002357 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 if options.emulate_svn_auto_props:
2359 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002360
2361 change_desc = None
2362
pgervais@chromium.org91141372014-01-09 23:27:20 +00002363 if options.email is not None:
2364 upload_args.extend(['--email', options.email])
2365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002367 if options.title:
2368 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002369 if options.message:
2370 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002371 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002372 print ("This branch is associated with issue %s. "
2373 "Adding patch to that issue." % cl.GetIssue())
2374 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002375 if options.title:
2376 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002377 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002378 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002379 if options.reviewers or options.tbr_owners:
2380 change_desc.update_reviewers(options.reviewers,
2381 options.tbr_owners,
2382 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002383 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002384 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002385
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002386 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387 print "Description is empty; aborting."
2388 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002389
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002390 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002391 if change_desc.get_reviewers():
2392 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002393 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002394 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002395 DieWithError("Must specify reviewers to send email.")
2396 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002397
2398 # We check this before applying rietveld.private assuming that in
2399 # rietveld.cc only addresses which we can send private CLs to are listed
2400 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2401 # --private is specified explicitly on the command line.
2402 if options.private:
2403 logging.warn('rietveld.cc is ignored since private flag is specified. '
2404 'You need to review and add them manually if necessary.')
2405 cc = cl.GetCCListWithoutDefault()
2406 else:
2407 cc = cl.GetCCList()
2408 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002409 if cc:
2410 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002412 if options.private or settings.GetDefaultPrivateFlag() == "True":
2413 upload_args.append('--private')
2414
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002415 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002416 if not options.find_copies:
2417 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002419 # Include the upstream repo's URL in the change -- this is useful for
2420 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002421 remote_url = cl.GetGitBaseUrlFromConfig()
2422 if not remote_url:
2423 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002424 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002425 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002426 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2427 remote_url = (cl.GetRemoteUrl() + '@'
2428 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002429 if remote_url:
2430 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002431 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002432 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2433 settings.GetPendingRefPrefix())
2434 if target_ref:
2435 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002437 # Look for dependent patchsets. See crbug.com/480453 for more details.
2438 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2439 upstream_branch = ShortBranchName(upstream_branch)
2440 if remote is '.':
2441 # A local branch is being tracked.
2442 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002443 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002444 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002445 print ('Skipping dependency patchset upload because git config '
2446 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002447 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002448 else:
2449 auth_config = auth.extract_auth_config_from_options(options)
2450 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2451 branch_cl_issue_url = branch_cl.GetIssueURL()
2452 branch_cl_issue = branch_cl.GetIssue()
2453 branch_cl_patchset = branch_cl.GetPatchset()
2454 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2455 upload_args.extend(
2456 ['--depends_on_patchset', '%s:%s' % (
2457 branch_cl_issue, branch_cl_patchset)])
2458 print
2459 print ('The current branch (%s) is tracking a local branch (%s) with '
2460 'an associated CL.') % (cl.GetBranch(), local_branch)
2461 print 'Adding %s/#ps%s as a dependency patchset.' % (
2462 branch_cl_issue_url, branch_cl_patchset)
2463 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002464
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002465 project = settings.GetProject()
2466 if project:
2467 upload_args.extend(['--project', project])
2468
rmistry@google.comef966222015-04-07 11:15:01 +00002469 if options.cq_dry_run:
2470 upload_args.extend(['--cq_dry_run'])
2471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002473 upload_args = ['upload'] + upload_args + args
2474 logging.info('upload.RealMain(%s)', upload_args)
2475 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002476 issue = int(issue)
2477 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002478 except KeyboardInterrupt:
2479 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002480 except:
2481 # If we got an exception after the user typed a description for their
2482 # change, back up the description before re-raising.
2483 if change_desc:
2484 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2485 print '\nGot exception while uploading -- saving description to %s\n' \
2486 % backup_path
2487 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002488 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489 backup_file.close()
2490 raise
2491
2492 if not cl.GetIssue():
2493 cl.SetIssue(issue)
2494 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002495
2496 if options.use_commit_queue:
2497 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498 return 0
2499
2500
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002501def cleanup_list(l):
2502 """Fixes a list so that comma separated items are put as individual items.
2503
2504 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2505 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2506 """
2507 items = sum((i.split(',') for i in l), [])
2508 stripped_items = (i.strip() for i in items)
2509 return sorted(filter(None, stripped_items))
2510
2511
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002512@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002513def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002514 """Uploads the current changelist to codereview.
2515
2516 Can skip dependency patchset uploads for a branch by running:
2517 git config branch.branch_name.skip-deps-uploads True
2518 To unset run:
2519 git config --unset branch.branch_name.skip-deps-uploads
2520 Can also set the above globally by using the --global flag.
2521 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002522 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2523 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002524 parser.add_option('--bypass-watchlists', action='store_true',
2525 dest='bypass_watchlists',
2526 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002527 parser.add_option('-f', action='store_true', dest='force',
2528 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002529 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002530 parser.add_option('-t', dest='title',
2531 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002532 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002533 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002534 help='reviewer email addresses')
2535 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002536 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002537 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002538 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002539 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002540 parser.add_option('--emulate_svn_auto_props',
2541 '--emulate-svn-auto-props',
2542 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002543 dest="emulate_svn_auto_props",
2544 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002545 parser.add_option('-c', '--use-commit-queue', action='store_true',
2546 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002547 parser.add_option('--private', action='store_true',
2548 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002549 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002550 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002551 metavar='TARGET',
2552 help='Apply CL to remote ref TARGET. ' +
2553 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002554 parser.add_option('--squash', action='store_true',
2555 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002556 parser.add_option('--no-squash', action='store_true',
2557 help='Don\'t squash multiple commits into one ' +
2558 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002559 parser.add_option('--email', default=None,
2560 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002561 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2562 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002563 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2564 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002565 help='Send the patchset to do a CQ dry run right after '
2566 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002567 parser.add_option('--dependencies', action='store_true',
2568 help='Uploads CLs of all the local branches that depend on '
2569 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002570
rmistry@google.com2dd99862015-06-22 12:22:18 +00002571 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002572 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002573 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002574 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002575 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002576
sbc@chromium.org71437c02015-04-09 19:29:40 +00002577 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002578 return 1
2579
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002580 options.reviewers = cleanup_list(options.reviewers)
2581 options.cc = cleanup_list(options.cc)
2582
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002583 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002584 if args:
2585 # TODO(ukai): is it ok for gerrit case?
2586 base_branch = args[0]
2587 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002588 if cl.GetBranch() is None:
2589 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2590
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002591 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002592 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002593 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002594
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002595 # Make sure authenticated to Rietveld before running expensive hooks. It is
2596 # a fast, best efforts check. Rietveld still can reject the authentication
2597 # during the actual upload.
2598 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2599 authenticator = auth.get_authenticator_for_host(
2600 cl.GetRietveldServer(), auth_config)
2601 if not authenticator.has_cached_credentials():
2602 raise auth.LoginRequiredError(cl.GetRietveldServer())
2603
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002604 # Apply watchlists on upload.
2605 change = cl.GetChange(base_branch, None)
2606 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2607 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002608 if not options.bypass_watchlists:
2609 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002610
ukai@chromium.orge8077812012-02-03 03:41:46 +00002611 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002612 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002613 # Set the reviewer list now so that presubmit checks can access it.
2614 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002615 change_description.update_reviewers(options.reviewers,
2616 options.tbr_owners,
2617 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002618 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002619 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002620 may_prompt=not options.force,
2621 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002622 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002623 if not hook_results.should_continue():
2624 return 1
2625 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002626 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002627
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002628 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002629 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002630 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002631 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002632 print ('The last upload made from this repository was patchset #%d but '
2633 'the most recent patchset on the server is #%d.'
2634 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002635 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2636 'from another machine or branch the patch you\'re uploading now '
2637 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002638 ask_for_data('About to upload; enter to confirm.')
2639
iannucci@chromium.org79540052012-10-19 23:15:26 +00002640 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002641 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002642 if options.squash and options.no_squash:
2643 DieWithError('Can only use one of --squash or --no-squash')
2644
2645 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2646 not options.no_squash)
2647
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002648 ret = GerritUpload(options, args, cl, change)
2649 else:
2650 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002651 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002652 git_set_branch_value('last-upload-hash',
2653 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002654 # Run post upload hooks, if specified.
2655 if settings.GetRunPostUploadHook():
2656 presubmit_support.DoPostUploadExecuter(
2657 change,
2658 cl,
2659 settings.GetRoot(),
2660 options.verbose,
2661 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002662
rmistry@google.com2dd99862015-06-22 12:22:18 +00002663 # Upload all dependencies if specified.
2664 if options.dependencies:
2665 print
2666 print '--dependencies has been specified.'
2667 print 'All dependent local branches will be re-uploaded.'
2668 print
2669 # Remove the dependencies flag from args so that we do not end up in a
2670 # loop.
2671 orig_args.remove('--dependencies')
2672 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002673 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002674
2675
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002676def IsSubmoduleMergeCommit(ref):
2677 # When submodules are added to the repo, we expect there to be a single
2678 # non-git-svn merge commit at remote HEAD with a signature comment.
2679 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002680 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002681 return RunGit(cmd) != ''
2682
2683
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002684def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002685 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002686
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002687 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002688 Updates changelog with metadata (e.g. pointer to review).
2689 Pushes/dcommits the code upstream.
2690 Updates review and closes.
2691 """
2692 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2693 help='bypass upload presubmit hook')
2694 parser.add_option('-m', dest='message',
2695 help="override review description")
2696 parser.add_option('-f', action='store_true', dest='force',
2697 help="force yes to questions (don't prompt)")
2698 parser.add_option('-c', dest='contributor',
2699 help="external contributor for patch (appended to " +
2700 "description and used as author for git). Should be " +
2701 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002702 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002703 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002705 auth_config = auth.extract_auth_config_from_options(options)
2706
2707 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002708
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002709 current = cl.GetBranch()
2710 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2711 if not settings.GetIsGitSvn() and remote == '.':
2712 print
2713 print 'Attempting to push branch %r into another local branch!' % current
2714 print
2715 print 'Either reparent this branch on top of origin/master:'
2716 print ' git reparent-branch --root'
2717 print
2718 print 'OR run `git rebase-update` if you think the parent branch is already'
2719 print 'committed.'
2720 print
2721 print ' Current parent: %r' % upstream_branch
2722 return 1
2723
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002724 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 # Default to merging against our best guess of the upstream branch.
2726 args = [cl.GetUpstreamBranch()]
2727
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002728 if options.contributor:
2729 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2730 print "Please provide contibutor as 'First Last <email@example.com>'"
2731 return 1
2732
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002734 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
sbc@chromium.org71437c02015-04-09 19:29:40 +00002736 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737 return 1
2738
2739 # This rev-list syntax means "show all commits not in my branch that
2740 # are in base_branch".
2741 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2742 base_branch]).splitlines()
2743 if upstream_commits:
2744 print ('Base branch "%s" has %d commits '
2745 'not in this branch.' % (base_branch, len(upstream_commits)))
2746 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2747 return 1
2748
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002749 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002750 svn_head = None
2751 if cmd == 'dcommit' or base_has_submodules:
2752 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2753 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002755 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002756 # If the base_head is a submodule merge commit, the first parent of the
2757 # base_head should be a git-svn commit, which is what we're interested in.
2758 base_svn_head = base_branch
2759 if base_has_submodules:
2760 base_svn_head += '^1'
2761
2762 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002763 if extra_commits:
2764 print ('This branch has %d additional commits not upstreamed yet.'
2765 % len(extra_commits.splitlines()))
2766 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2767 'before attempting to %s.' % (base_branch, cmd))
2768 return 1
2769
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002770 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002771 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002772 author = None
2773 if options.contributor:
2774 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002775 hook_results = cl.RunHook(
2776 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002777 may_prompt=not options.force,
2778 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002779 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002780 if not hook_results.should_continue():
2781 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002783 # Check the tree status if the tree status URL is set.
2784 status = GetTreeStatus()
2785 if 'closed' == status:
2786 print('The tree is closed. Please wait for it to reopen. Use '
2787 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2788 return 1
2789 elif 'unknown' == status:
2790 print('Unable to determine tree status. Please verify manually and '
2791 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2792 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002793
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002794 change_desc = ChangeDescription(options.message)
2795 if not change_desc.description and cl.GetIssue():
2796 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002798 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002799 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002800 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002801 else:
2802 print 'No description set.'
2803 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2804 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002806 # Keep a separate copy for the commit message, because the commit message
2807 # contains the link to the Rietveld issue, while the Rietveld message contains
2808 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002809 # Keep a separate copy for the commit message.
2810 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002811 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002812
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002813 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002814 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002815 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002816 # after it. Add a period on a new line to circumvent this. Also add a space
2817 # before the period to make sure that Gitiles continues to correctly resolve
2818 # the URL.
2819 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002821 commit_desc.append_footer('Patch from %s.' % options.contributor)
2822
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002823 print('Description:')
2824 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002825
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002826 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002828 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002830 # We want to squash all this branch's commits into one commit with the proper
2831 # description. We do this by doing a "reset --soft" to the base branch (which
2832 # keeps the working copy the same), then dcommitting that. If origin/master
2833 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2834 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002836 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2837 # Delete the branches if they exist.
2838 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2839 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2840 result = RunGitWithCode(showref_cmd)
2841 if result[0] == 0:
2842 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002843
2844 # We might be in a directory that's present in this branch but not in the
2845 # trunk. Move up to the top of the tree so that git commands that expect a
2846 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002847 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002848 if rel_base_path:
2849 os.chdir(rel_base_path)
2850
2851 # Stuff our change into the merge branch.
2852 # We wrap in a try...finally block so if anything goes wrong,
2853 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002854 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002855 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002856 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002857 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002859 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002860 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002862 RunGit(
2863 [
2864 'commit', '--author', options.contributor,
2865 '-m', commit_desc.description,
2866 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002868 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002869 if base_has_submodules:
2870 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2871 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2872 RunGit(['checkout', CHERRY_PICK_BRANCH])
2873 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002874 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002875 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002876 mirror = settings.GetGitMirror(remote)
2877 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002878 pending_prefix = settings.GetPendingRefPrefix()
2879 if not pending_prefix or branch.startswith(pending_prefix):
2880 # If not using refs/pending/heads/* at all, or target ref is already set
2881 # to pending, then push to the target ref directly.
2882 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002883 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002884 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002885 else:
2886 # Cherry-pick the change on top of pending ref and then push it.
2887 assert branch.startswith('refs/'), branch
2888 assert pending_prefix[-1] == '/', pending_prefix
2889 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002890 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002891 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002892 if retcode == 0:
2893 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894 else:
2895 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002896 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002897 'svn', 'dcommit',
2898 '-C%s' % options.similarity,
2899 '--no-rebase', '--rmdir',
2900 ]
2901 if settings.GetForceHttpsCommitUrl():
2902 # Allow forcing https commit URLs for some projects that don't allow
2903 # committing to http URLs (like Google Code).
2904 remote_url = cl.GetGitSvnRemoteUrl()
2905 if urlparse.urlparse(remote_url).scheme == 'http':
2906 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002907 cmd_args.append('--commit-url=%s' % remote_url)
2908 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002909 if 'Committed r' in output:
2910 revision = re.match(
2911 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2912 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 finally:
2914 # And then swap back to the original branch and clean up.
2915 RunGit(['checkout', '-q', cl.GetBranch()])
2916 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002917 if base_has_submodules:
2918 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002919
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002920 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002921 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002922 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002923
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002924 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002925 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002926 try:
2927 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2928 # We set pushed_to_pending to False, since it made it all the way to the
2929 # real ref.
2930 pushed_to_pending = False
2931 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002932 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002935 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002936 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002937 if not to_pending:
2938 if viewvc_url and revision:
2939 change_desc.append_footer(
2940 'Committed: %s%s' % (viewvc_url, revision))
2941 elif revision:
2942 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943 print ('Closing issue '
2944 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002945 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002947 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002948 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002949 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002950 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002951 if options.bypass_hooks:
2952 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2953 else:
2954 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002955 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002956 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002957
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002958 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002959 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2960 print 'The commit is in the pending queue (%s).' % pending_ref
2961 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002962 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002963 'footer.' % branch)
2964
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002965 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2966 if os.path.isfile(hook):
2967 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002968
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002969 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970
2971
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002972def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2973 print
2974 print 'Waiting for commit to be landed on %s...' % real_ref
2975 print '(If you are impatient, you may Ctrl-C once without harm)'
2976 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2977 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002978 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002979
2980 loop = 0
2981 while True:
2982 sys.stdout.write('fetching (%d)... \r' % loop)
2983 sys.stdout.flush()
2984 loop += 1
2985
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002986 if mirror:
2987 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002988 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2989 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2990 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2991 for commit in commits.splitlines():
2992 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2993 print 'Found commit on %s' % real_ref
2994 return commit
2995
2996 current_rev = to_rev
2997
2998
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002999def PushToGitPending(remote, pending_ref, upstream_ref):
3000 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3001
3002 Returns:
3003 (retcode of last operation, output log of last operation).
3004 """
3005 assert pending_ref.startswith('refs/'), pending_ref
3006 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3007 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3008 code = 0
3009 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003010 max_attempts = 3
3011 attempts_left = max_attempts
3012 while attempts_left:
3013 if attempts_left != max_attempts:
3014 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3015 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003016
3017 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003018 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003019 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003020 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003021 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003022 print 'Fetch failed with exit code %d.' % code
3023 if out.strip():
3024 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003025 continue
3026
3027 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003028 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003029 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003030 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003031 if code:
3032 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003033 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3034 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003035 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3036 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003037 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003038 return code, out
3039
3040 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003041 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003042 code, out = RunGitWithCode(
3043 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3044 if code == 0:
3045 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003046 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003047 return code, out
3048
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003049 print 'Push failed with exit code %d.' % code
3050 if out.strip():
3051 print out.strip()
3052 if IsFatalPushFailure(out):
3053 print (
3054 'Fatal push error. Make sure your .netrc credentials and git '
3055 'user.email are correct and you have push access to the repo.')
3056 return code, out
3057
3058 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003059 return code, out
3060
3061
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003062def IsFatalPushFailure(push_stdout):
3063 """True if retrying push won't help."""
3064 return '(prohibited by Gerrit)' in push_stdout
3065
3066
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003067@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003068def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003069 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003070 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003071 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003072 # If it looks like previous commits were mirrored with git-svn.
3073 message = """This repository appears to be a git-svn mirror, but no
3074upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3075 else:
3076 message = """This doesn't appear to be an SVN repository.
3077If your project has a true, writeable git repository, you probably want to run
3078'git cl land' instead.
3079If your project has a git mirror of an upstream SVN master, you probably need
3080to run 'git svn init'.
3081
3082Using the wrong command might cause your commit to appear to succeed, and the
3083review to be closed, without actually landing upstream. If you choose to
3084proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003085 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003086 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003087 return SendUpstream(parser, args, 'dcommit')
3088
3089
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003090@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003091def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003092 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003093 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003094 print('This appears to be an SVN repository.')
3095 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003096 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003097 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003098 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099
3100
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003101def ParseIssueNum(arg):
3102 """Parses the issue number from args if present otherwise returns None."""
3103 if re.match(r'\d+', arg):
3104 return arg
3105 if arg.startswith('http'):
3106 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3107 return None
3108
3109
3110@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003111def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003112 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003113 parser.add_option('-b', dest='newbranch',
3114 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003115 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003117 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3118 help='Change to the directory DIR immediately, '
3119 'before doing anything else.')
3120 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003121 help='failed patches spew .rej files rather than '
3122 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3124 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003125
3126 group = optparse.OptionGroup(parser,
3127 """Options for continuing work on the current issue uploaded
3128from a different clone (e.g. different machine). Must be used independently from
3129the other options. No issue number should be specified, and the branch must have
3130an issue number associated with it""")
3131 group.add_option('--reapply', action='store_true',
3132 dest='reapply',
3133 help="""Reset the branch and reapply the issue.
3134CAUTION: This will undo any local changes in this branch""")
3135
3136 group.add_option('--pull', action='store_true', dest='pull',
3137 help="Performs a pull before reapplying.")
3138 parser.add_option_group(group)
3139
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003140 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003141 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003142 auth_config = auth.extract_auth_config_from_options(options)
3143
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003144 issue_arg = None
3145 if options.reapply :
3146 if len(args) > 0:
3147 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003148
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003149 cl = Changelist()
3150 issue_arg = cl.GetIssue()
3151 upstream = cl.GetUpstreamBranch()
3152 if upstream == None:
3153 parser.error("No upstream branch specified. Cannot reset branch")
3154
3155 RunGit(['reset', '--hard', upstream])
3156 if options.pull:
3157 RunGit(['pull'])
3158 else:
3159 if len(args) != 1:
3160 parser.error("Must specify issue number")
3161
3162 issue_arg = ParseIssueNum(args[0])
3163
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003164 # The patch URL works because ParseIssueNum won't do any substitution
3165 # as the re.sub pattern fails to match and just returns it.
3166 if issue_arg == None:
3167 parser.print_help()
3168 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003170 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003171 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003172 return 1
3173
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003174 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003175 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003176
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003177 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003178 if options.reapply:
3179 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003180 if options.force:
3181 RunGit(['branch', '-D', options.newbranch],
3182 stderr=subprocess2.PIPE, error_ok=True)
3183 RunGit(['checkout', '-b', options.newbranch,
3184 Changelist().GetUpstreamBranch()])
3185
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003186 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003187 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003188
3189
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003190def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003191 # PatchIssue should never be called with a dirty tree. It is up to the
3192 # caller to check this, but just in case we assert here since the
3193 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003194 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003195
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003196 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003197 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003198 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003199 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003200 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003201 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003202 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003203 # Assume it's a URL to the patch. Default to https.
3204 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003205 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003206 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003207 DieWithError('Must pass an issue ID or full URL for '
3208 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003209 issue = int(match.group(2))
3210 cl = Changelist(issue=issue, auth_config=auth_config)
3211 cl.rietveld_server = match.group(1)
3212 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003213 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003215 # Switch up to the top-level directory, if necessary, in preparation for
3216 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003217 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218 if top:
3219 os.chdir(top)
3220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 # Git patches have a/ at the beginning of source paths. We strip that out
3222 # with a sed script rather than the -p flag to patch so we can feed either
3223 # Git or svn-style patches into the same apply command.
3224 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003225 try:
3226 patch_data = subprocess2.check_output(
3227 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3228 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 DieWithError('Git patch mungling failed.')
3230 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 # We use "git apply" to apply the patch instead of "patch" so that we can
3233 # pick up file adds.
3234 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003235 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003236 if directory:
3237 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003238 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003239 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003240 elif IsGitVersionAtLeast('1.7.12'):
3241 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003242 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003243 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003244 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003245 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003246 print 'Failed to apply the patch'
3247 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248
3249 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003250 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003251 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3252 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003253 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3254 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003255 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003257 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003258 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 else:
3260 print "Patch applied to index."
3261 return 0
3262
3263
3264def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003265 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266 # Provide a wrapper for git svn rebase to help avoid accidental
3267 # git svn dcommit.
3268 # It's the only command that doesn't use parser at all since we just defer
3269 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003270
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003271 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272
3273
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003274def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003275 """Fetches the tree status and returns either 'open', 'closed',
3276 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003277 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003278 if url:
3279 status = urllib2.urlopen(url).read().lower()
3280 if status.find('closed') != -1 or status == '0':
3281 return 'closed'
3282 elif status.find('open') != -1 or status == '1':
3283 return 'open'
3284 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285 return 'unset'
3286
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288def GetTreeStatusReason():
3289 """Fetches the tree status from a json url and returns the message
3290 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003291 url = settings.GetTreeStatusUrl()
3292 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293 connection = urllib2.urlopen(json_url)
3294 status = json.loads(connection.read())
3295 connection.close()
3296 return status['message']
3297
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003298
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003299def GetBuilderMaster(bot_list):
3300 """For a given builder, fetch the master from AE if available."""
3301 map_url = 'https://builders-map.appspot.com/'
3302 try:
3303 master_map = json.load(urllib2.urlopen(map_url))
3304 except urllib2.URLError as e:
3305 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3306 (map_url, e))
3307 except ValueError as e:
3308 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3309 if not master_map:
3310 return None, 'Failed to build master map.'
3311
3312 result_master = ''
3313 for bot in bot_list:
3314 builder = bot.split(':', 1)[0]
3315 master_list = master_map.get(builder, [])
3316 if not master_list:
3317 return None, ('No matching master for builder %s.' % builder)
3318 elif len(master_list) > 1:
3319 return None, ('The builder name %s exists in multiple masters %s.' %
3320 (builder, master_list))
3321 else:
3322 cur_master = master_list[0]
3323 if not result_master:
3324 result_master = cur_master
3325 elif result_master != cur_master:
3326 return None, 'The builders do not belong to the same master.'
3327 return result_master, None
3328
3329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003331 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003332 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333 status = GetTreeStatus()
3334 if 'unset' == status:
3335 print 'You must configure your tree status URL by running "git cl config".'
3336 return 2
3337
3338 print "The tree is %s" % status
3339 print
3340 print GetTreeStatusReason()
3341 if status != 'open':
3342 return 1
3343 return 0
3344
3345
maruel@chromium.org15192402012-09-06 12:38:29 +00003346def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003347 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003348 group = optparse.OptionGroup(parser, "Try job options")
3349 group.add_option(
3350 "-b", "--bot", action="append",
3351 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3352 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003353 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003354 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003355 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003356 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003357 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003358 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003359 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003360 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003361 "-r", "--revision",
3362 help="Revision to use for the try job; default: the "
3363 "revision will be determined by the try server; see "
3364 "its waterfall for more info")
3365 group.add_option(
3366 "-c", "--clobber", action="store_true", default=False,
3367 help="Force a clobber before building; e.g. don't do an "
3368 "incremental build")
3369 group.add_option(
3370 "--project",
3371 help="Override which project to use. Projects are defined "
3372 "server-side to define what default bot set to use")
3373 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003374 "-p", "--property", dest="properties", action="append", default=[],
3375 help="Specify generic properties in the form -p key1=value1 -p "
3376 "key2=value2 etc (buildbucket only). The value will be treated as "
3377 "json if decodable, or as string otherwise.")
3378 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003379 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003380 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003381 "--use-rietveld", action="store_true", default=False,
3382 help="Use Rietveld to trigger try jobs.")
3383 group.add_option(
3384 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3385 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003386 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003387 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003388 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003389 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003390
machenbach@chromium.org45453142015-09-15 08:45:22 +00003391 if options.use_rietveld and options.properties:
3392 parser.error('Properties can only be specified with buildbucket')
3393
3394 # Make sure that all properties are prop=value pairs.
3395 bad_params = [x for x in options.properties if '=' not in x]
3396 if bad_params:
3397 parser.error('Got properties with missing "=": %s' % bad_params)
3398
maruel@chromium.org15192402012-09-06 12:38:29 +00003399 if args:
3400 parser.error('Unknown arguments: %s' % args)
3401
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003402 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003403 if not cl.GetIssue():
3404 parser.error('Need to upload first')
3405
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003406 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003407 if props.get('closed'):
3408 parser.error('Cannot send tryjobs for a closed CL')
3409
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003410 if props.get('private'):
3411 parser.error('Cannot use trybots with private issue')
3412
maruel@chromium.org15192402012-09-06 12:38:29 +00003413 if not options.name:
3414 options.name = cl.GetBranch()
3415
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003416 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003417 options.master, err_msg = GetBuilderMaster(options.bot)
3418 if err_msg:
3419 parser.error('Tryserver master cannot be found because: %s\n'
3420 'Please manually specify the tryserver master'
3421 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003422
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003423 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003424 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003425 if not options.bot:
3426 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003427
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003428 # Get try masters from PRESUBMIT.py files.
3429 masters = presubmit_support.DoGetTryMasters(
3430 change,
3431 change.LocalPaths(),
3432 settings.GetRoot(),
3433 None,
3434 None,
3435 options.verbose,
3436 sys.stdout)
3437 if masters:
3438 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003439
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003440 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3441 options.bot = presubmit_support.DoGetTrySlaves(
3442 change,
3443 change.LocalPaths(),
3444 settings.GetRoot(),
3445 None,
3446 None,
3447 options.verbose,
3448 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003449
3450 if not options.bot:
3451 # Get try masters from cq.cfg if any.
3452 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3453 # location.
3454 cq_cfg = os.path.join(change.RepositoryRoot(),
3455 'infra', 'config', 'cq.cfg')
3456 if os.path.exists(cq_cfg):
3457 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003458 cq_masters = commit_queue.get_master_builder_map(
3459 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003460 for master, builders in cq_masters.iteritems():
3461 for builder in builders:
3462 # Skip presubmit builders, because these will fail without LGTM.
3463 if 'presubmit' not in builder.lower():
3464 masters.setdefault(master, {})[builder] = ['defaulttests']
3465 if masters:
3466 return masters
3467
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003468 if not options.bot:
3469 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003470
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003471 builders_and_tests = {}
3472 # TODO(machenbach): The old style command-line options don't support
3473 # multiple try masters yet.
3474 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3475 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3476
3477 for bot in old_style:
3478 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003479 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003480 elif ',' in bot:
3481 parser.error('Specify one bot per --bot flag')
3482 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003483 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003484
3485 for bot, tests in new_style:
3486 builders_and_tests.setdefault(bot, []).extend(tests)
3487
3488 # Return a master map with one master to be backwards compatible. The
3489 # master name defaults to an empty string, which will cause the master
3490 # not to be set on rietveld (deprecated).
3491 return {options.master: builders_and_tests}
3492
3493 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003494
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003495 for builders in masters.itervalues():
3496 if any('triggered' in b for b in builders):
3497 print >> sys.stderr, (
3498 'ERROR You are trying to send a job to a triggered bot. This type of'
3499 ' bot requires an\ninitial job from a parent (usually a builder). '
3500 'Instead send your job to the parent.\n'
3501 'Bot list: %s' % builders)
3502 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003503
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003504 patchset = cl.GetMostRecentPatchset()
3505 if patchset and patchset != cl.GetPatchset():
3506 print(
3507 '\nWARNING Mismatch between local config and server. Did a previous '
3508 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3509 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003510 if options.luci:
3511 trigger_luci_job(cl, masters, options)
3512 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003513 try:
3514 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3515 except BuildbucketResponseException as ex:
3516 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003517 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003518 except Exception as e:
3519 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3520 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3521 e, stacktrace)
3522 return 1
3523 else:
3524 try:
3525 cl.RpcServer().trigger_distributed_try_jobs(
3526 cl.GetIssue(), patchset, options.name, options.clobber,
3527 options.revision, masters)
3528 except urllib2.HTTPError as e:
3529 if e.code == 404:
3530 print('404 from rietveld; '
3531 'did you mean to use "git try" instead of "git cl try"?')
3532 return 1
3533 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003534
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003535 for (master, builders) in sorted(masters.iteritems()):
3536 if master:
3537 print 'Master: %s' % master
3538 length = max(len(builder) for builder in builders)
3539 for builder in sorted(builders):
3540 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003541 return 0
3542
3543
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003544def CMDtry_results(parser, args):
3545 group = optparse.OptionGroup(parser, "Try job results options")
3546 group.add_option(
3547 "-p", "--patchset", type=int, help="patchset number if not current.")
3548 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003549 "--print-master", action='store_true', help="print master name as well.")
3550 group.add_option(
3551 "--color", action='store_true', default=sys.stdout.isatty(),
3552 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003553 group.add_option(
3554 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3555 help="Host of buildbucket. The default host is %default.")
3556 parser.add_option_group(group)
3557 auth.add_auth_options(parser)
3558 options, args = parser.parse_args(args)
3559 if args:
3560 parser.error('Unrecognized args: %s' % ' '.join(args))
3561
3562 auth_config = auth.extract_auth_config_from_options(options)
3563 cl = Changelist(auth_config=auth_config)
3564 if not cl.GetIssue():
3565 parser.error('Need to upload first')
3566
3567 if not options.patchset:
3568 options.patchset = cl.GetMostRecentPatchset()
3569 if options.patchset and options.patchset != cl.GetPatchset():
3570 print(
3571 '\nWARNING Mismatch between local config and server. Did a previous '
3572 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3573 'Continuing using\npatchset %s.\n' % options.patchset)
3574 try:
3575 jobs = fetch_try_jobs(auth_config, cl, options)
3576 except BuildbucketResponseException as ex:
3577 print 'Buildbucket error: %s' % ex
3578 return 1
3579 except Exception as e:
3580 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3581 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3582 e, stacktrace)
3583 return 1
3584 print_tryjobs(options, jobs)
3585 return 0
3586
3587
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003588@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003589def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003590 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003591 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003592 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003593 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003594
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003596 if args:
3597 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003598 branch = cl.GetBranch()
3599 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003600 cl = Changelist()
3601 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003602
3603 # Clear configured merge-base, if there is one.
3604 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003605 else:
3606 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003607 return 0
3608
3609
thestig@chromium.org00858c82013-12-02 23:08:03 +00003610def CMDweb(parser, args):
3611 """Opens the current CL in the web browser."""
3612 _, args = parser.parse_args(args)
3613 if args:
3614 parser.error('Unrecognized args: %s' % ' '.join(args))
3615
3616 issue_url = Changelist().GetIssueURL()
3617 if not issue_url:
3618 print >> sys.stderr, 'ERROR No issue to open'
3619 return 1
3620
3621 webbrowser.open(issue_url)
3622 return 0
3623
3624
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003625def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003626 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003627 auth.add_auth_options(parser)
3628 options, args = parser.parse_args(args)
3629 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003630 if args:
3631 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003632 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003633 props = cl.GetIssueProperties()
3634 if props.get('private'):
3635 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003636 cl.SetFlag('commit', '1')
3637 return 0
3638
3639
groby@chromium.org411034a2013-02-26 15:12:01 +00003640def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003641 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003642 auth.add_auth_options(parser)
3643 options, args = parser.parse_args(args)
3644 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003645 if args:
3646 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003647 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003648 # Ensure there actually is an issue to close.
3649 cl.GetDescription()
3650 cl.CloseIssue()
3651 return 0
3652
3653
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003654def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003655 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003656 auth.add_auth_options(parser)
3657 options, args = parser.parse_args(args)
3658 auth_config = auth.extract_auth_config_from_options(options)
3659 if args:
3660 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003661
3662 # Uncommitted (staged and unstaged) changes will be destroyed by
3663 # "git reset --hard" if there are merging conflicts in PatchIssue().
3664 # Staged changes would be committed along with the patch from last
3665 # upload, hence counted toward the "last upload" side in the final
3666 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003667 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003668 return 1
3669
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003670 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003671 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003672 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003673 if not issue:
3674 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003675 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003676 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003677
3678 # Create a new branch based on the merge-base
3679 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3680 try:
3681 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003682 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003683 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003684 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003685 return rtn
3686
wychen@chromium.org06928532015-02-03 02:11:29 +00003687 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003688 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003689 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003690 finally:
3691 RunGit(['checkout', '-q', branch])
3692 RunGit(['branch', '-D', TMP_BRANCH])
3693
3694 return 0
3695
3696
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003697def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003698 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003699 parser.add_option(
3700 '--no-color',
3701 action='store_true',
3702 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003703 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003704 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003705 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003706
3707 author = RunGit(['config', 'user.email']).strip() or None
3708
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003709 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003710
3711 if args:
3712 if len(args) > 1:
3713 parser.error('Unknown args')
3714 base_branch = args[0]
3715 else:
3716 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003717 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003718
3719 change = cl.GetChange(base_branch, None)
3720 return owners_finder.OwnersFinder(
3721 [f.LocalPath() for f in
3722 cl.GetChange(base_branch, None).AffectedFiles()],
3723 change.RepositoryRoot(), author,
3724 fopen=file, os_path=os.path, glob=glob.glob,
3725 disable_color=options.no_color).run()
3726
3727
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003728def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003729 """Generates a diff command."""
3730 # Generate diff for the current branch's changes.
3731 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3732 upstream_commit, '--' ]
3733
3734 if args:
3735 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003736 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003737 diff_cmd.append(arg)
3738 else:
3739 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003740
3741 return diff_cmd
3742
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003743def MatchingFileType(file_name, extensions):
3744 """Returns true if the file name ends with one of the given extensions."""
3745 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003746
enne@chromium.org555cfe42014-01-29 18:21:39 +00003747@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003748def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003749 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003750 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003751 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003752 parser.add_option('--full', action='store_true',
3753 help='Reformat the full content of all touched files')
3754 parser.add_option('--dry-run', action='store_true',
3755 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003756 parser.add_option('--python', action='store_true',
3757 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003758 parser.add_option('--diff', action='store_true',
3759 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003760 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003761
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003762 # git diff generates paths against the root of the repository. Change
3763 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003764 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003765 if rel_base_path:
3766 os.chdir(rel_base_path)
3767
digit@chromium.org29e47272013-05-17 17:01:46 +00003768 # Grab the merge-base commit, i.e. the upstream commit of the current
3769 # branch when it was created or the last time it was rebased. This is
3770 # to cover the case where the user may have called "git fetch origin",
3771 # moving the origin branch to a newer commit, but hasn't rebased yet.
3772 upstream_commit = None
3773 cl = Changelist()
3774 upstream_branch = cl.GetUpstreamBranch()
3775 if upstream_branch:
3776 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3777 upstream_commit = upstream_commit.strip()
3778
3779 if not upstream_commit:
3780 DieWithError('Could not find base commit for this branch. '
3781 'Are you in detached state?')
3782
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003783 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3784 diff_output = RunGit(changed_files_cmd)
3785 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003786 # Filter out files deleted by this CL
3787 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003788
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003789 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3790 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3791 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003792 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003793
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003794 top_dir = os.path.normpath(
3795 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3796
3797 # Locate the clang-format binary in the checkout
3798 try:
3799 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3800 except clang_format.NotFoundError, e:
3801 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003802
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003803 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3804 # formatted. This is used to block during the presubmit.
3805 return_value = 0
3806
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003807 if clang_diff_files:
3808 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003809 cmd = [clang_format_tool]
3810 if not opts.dry_run and not opts.diff:
3811 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003812 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003813 if opts.diff:
3814 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003815 else:
3816 env = os.environ.copy()
3817 env['PATH'] = str(os.path.dirname(clang_format_tool))
3818 try:
3819 script = clang_format.FindClangFormatScriptInChromiumTree(
3820 'clang-format-diff.py')
3821 except clang_format.NotFoundError, e:
3822 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003823
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003824 cmd = [sys.executable, script, '-p0']
3825 if not opts.dry_run and not opts.diff:
3826 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003827
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003828 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3829 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003830
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003831 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3832 if opts.diff:
3833 sys.stdout.write(stdout)
3834 if opts.dry_run and len(stdout) > 0:
3835 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003836
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003837 # Similar code to above, but using yapf on .py files rather than clang-format
3838 # on C/C++ files
3839 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003840 yapf_tool = gclient_utils.FindExecutable('yapf')
3841 if yapf_tool is None:
3842 DieWithError('yapf not found in PATH')
3843
3844 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003845 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003846 cmd = [yapf_tool]
3847 if not opts.dry_run and not opts.diff:
3848 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003849 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003850 if opts.diff:
3851 sys.stdout.write(stdout)
3852 else:
3853 # TODO(sbc): yapf --lines mode still has some issues.
3854 # https://github.com/google/yapf/issues/154
3855 DieWithError('--python currently only works with --full')
3856
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003857 # Dart's formatter does not have the nice property of only operating on
3858 # modified chunks, so hard code full.
3859 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003860 try:
3861 command = [dart_format.FindDartFmtToolInChromiumTree()]
3862 if not opts.dry_run and not opts.diff:
3863 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003864 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003865
ppi@chromium.org6593d932016-03-03 15:41:15 +00003866 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003867 if opts.dry_run and stdout:
3868 return_value = 2
3869 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003870 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3871 'found in this checkout. Files in other languages are still ' +
3872 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003873
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003874 # Format GN build files. Always run on full build files for canonical form.
3875 if gn_diff_files:
3876 cmd = ['gn', 'format']
3877 if not opts.dry_run and not opts.diff:
3878 cmd.append('--in-place')
3879 for gn_diff_file in gn_diff_files:
3880 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3881 if opts.diff:
3882 sys.stdout.write(stdout)
3883
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003884 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003885
3886
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003887@subcommand.usage('<codereview url or issue id>')
3888def CMDcheckout(parser, args):
3889 """Checks out a branch associated with a given Rietveld issue."""
3890 _, args = parser.parse_args(args)
3891
3892 if len(args) != 1:
3893 parser.print_help()
3894 return 1
3895
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003896 target_issue = ParseIssueNum(args[0])
3897 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003898 parser.print_help()
3899 return 1
3900
3901 key_and_issues = [x.split() for x in RunGit(
3902 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3903 .splitlines()]
3904 branches = []
3905 for key, issue in key_and_issues:
3906 if issue == target_issue:
3907 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3908
3909 if len(branches) == 0:
3910 print 'No branch found for issue %s.' % target_issue
3911 return 1
3912 if len(branches) == 1:
3913 RunGit(['checkout', branches[0]])
3914 else:
3915 print 'Multiple branches match issue %s:' % target_issue
3916 for i in range(len(branches)):
3917 print '%d: %s' % (i, branches[i])
3918 which = raw_input('Choose by index: ')
3919 try:
3920 RunGit(['checkout', branches[int(which)]])
3921 except (IndexError, ValueError):
3922 print 'Invalid selection, not checking out any branch.'
3923 return 1
3924
3925 return 0
3926
3927
maruel@chromium.org29404b52014-09-08 22:58:00 +00003928def CMDlol(parser, args):
3929 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003930 print zlib.decompress(base64.b64decode(
3931 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3932 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3933 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3934 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003935 return 0
3936
3937
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003938class OptionParser(optparse.OptionParser):
3939 """Creates the option parse and add --verbose support."""
3940 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003941 optparse.OptionParser.__init__(
3942 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003943 self.add_option(
3944 '-v', '--verbose', action='count', default=0,
3945 help='Use 2 times for more debugging info')
3946
3947 def parse_args(self, args=None, values=None):
3948 options, args = optparse.OptionParser.parse_args(self, args, values)
3949 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3950 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3951 return options, args
3952
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003955 if sys.hexversion < 0x02060000:
3956 print >> sys.stderr, (
3957 '\nYour python version %s is unsupported, please upgrade.\n' %
3958 sys.version.split(' ', 1)[0])
3959 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003960
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003961 # Reload settings.
3962 global settings
3963 settings = Settings()
3964
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003965 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003966 dispatcher = subcommand.CommandDispatcher(__name__)
3967 try:
3968 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003969 except auth.AuthenticationError as e:
3970 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003971 except urllib2.HTTPError, e:
3972 if e.code != 500:
3973 raise
3974 DieWithError(
3975 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3976 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003977 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978
3979
3980if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003981 # These affect sys.stdout so do it outside of main() to simplify mocks in
3982 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003983 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003984 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003985 try:
3986 sys.exit(main(sys.argv[1:]))
3987 except KeyboardInterrupt:
3988 sys.stderr.write('interrupted\n')
3989 sys.exit(1)