blob: 1b6d4f47509578e0923e85b2e5c84ab6491582e4 [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
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001086 def GetGerritServer(self):
1087 # We don't support multiple Gerrit servers, and assume it to be same as
1088 # origin, except with a '-review' suffix for first subdomain.
1089 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1090 parts[0] = parts[0] + '-review'
1091 return 'https://%s' % '.'.join(parts)
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 def GetIssueURL(self):
1094 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001095 if not self.GetIssue():
1096 return None
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001097 if settings.GetIsGerrit():
1098 return '%s/%s' % (self.GetGerritServer(), self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1100
1101 def GetDescription(self, pretty=False):
1102 if not self.has_description:
1103 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001104 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001105 try:
1106 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001107 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001108 if e.code == 404:
1109 DieWithError(
1110 ('\nWhile fetching the description for issue %d, received a '
1111 '404 (not found)\n'
1112 'error. It is likely that you deleted this '
1113 'issue on the server. If this is the\n'
1114 'case, please run\n\n'
1115 ' git cl issue 0\n\n'
1116 'to clear the association with the deleted issue. Then run '
1117 'this command again.') % issue)
1118 else:
1119 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001120 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001121 except urllib2.URLError as e:
1122 print >> sys.stderr, (
1123 'Warning: Failed to retrieve CL description due to network '
1124 'failure.')
1125 self.description = ''
1126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.has_description = True
1128 if pretty:
1129 wrapper = textwrap.TextWrapper()
1130 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1131 return wrapper.fill(self.description)
1132 return self.description
1133
1134 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001135 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 patchset = RunGit(['config', self._PatchsetSetting()],
1138 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.patchset = int(patchset) or None if patchset else None
1140 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 return self.patchset
1142
1143 def SetPatchset(self, patchset):
1144 """Set this branch's patchset. If patchset=0, clears the patchset."""
1145 if patchset:
1146 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 else:
1149 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001150 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001151 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001153 def GetMostRecentPatchset(self):
1154 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001155
1156 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001157 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001158 '/download/issue%s_%s.diff' % (issue, patchset))
1159
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 def GetIssueProperties(self):
1161 if self._props is None:
1162 issue = self.GetIssue()
1163 if not issue:
1164 self._props = {}
1165 else:
1166 self._props = self.RpcServer().get_issue_properties(issue, True)
1167 return self._props
1168
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001169 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001170 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001171
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001172 def AddComment(self, message):
1173 return self.RpcServer().add_comment(self.GetIssue(), message)
1174
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001175 def SetIssue(self, issue=None):
1176 """Set this branch's issue. If issue isn't given, clears the issue."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001178 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 RunGit(['config', self._IssueSetting(), str(issue)])
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001180 if not settings.GetIsGerrit() and self.rietveld_server:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1182 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001183 current_issue = self.GetIssue()
1184 if current_issue:
1185 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001186 self.issue = None
1187 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001189 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 if not self.GitSanityChecks(upstream_branch):
1191 DieWithError('\nGit sanity check failure')
1192
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001193 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001194 if not root:
1195 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001196 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001197
1198 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001200 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001201 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001202 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001203 except subprocess2.CalledProcessError:
1204 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001205 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001206 'This branch probably doesn\'t exist anymore. To reset the\n'
1207 'tracking branch, please run\n'
1208 ' git branch --set-upstream %s trunk\n'
1209 'replacing trunk with origin/master or the relevant branch') %
1210 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001211
maruel@chromium.org52424302012-08-29 15:14:30 +00001212 issue = self.GetIssue()
1213 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001214 if issue:
1215 description = self.GetDescription()
1216 else:
1217 # If the change was never uploaded, use the log messages of all commits
1218 # up to the branch point, as git cl upload will prefill the description
1219 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001220 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1221 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001222
1223 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001224 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001225 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001226 name,
1227 description,
1228 absroot,
1229 files,
1230 issue,
1231 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001232 author,
1233 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001234
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001235 def GetStatus(self):
1236 """Apply a rough heuristic to give a simple summary of an issue's review
1237 or CQ status, assuming adherence to a common workflow.
1238
1239 Returns None if no issue for this branch, or one of the following keywords:
1240 * 'error' - error from review tool (including deleted issues)
1241 * 'unsent' - not sent for review
1242 * 'waiting' - waiting for review
1243 * 'reply' - waiting for owner to reply to review
1244 * 'lgtm' - LGTM from at least one approved reviewer
1245 * 'commit' - in the commit queue
1246 * 'closed' - closed
1247 """
1248 if not self.GetIssue():
1249 return None
1250
1251 try:
1252 props = self.GetIssueProperties()
1253 except urllib2.HTTPError:
1254 return 'error'
1255
1256 if props.get('closed'):
1257 # Issue is closed.
1258 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001259 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001260 # Issue is in the commit queue.
1261 return 'commit'
1262
1263 try:
1264 reviewers = self.GetApprovingReviewers()
1265 except urllib2.HTTPError:
1266 return 'error'
1267
1268 if reviewers:
1269 # Was LGTM'ed.
1270 return 'lgtm'
1271
1272 messages = props.get('messages') or []
1273
1274 if not messages:
1275 # No message was sent.
1276 return 'unsent'
1277 if messages[-1]['sender'] != props.get('owner_email'):
1278 # Non-LGTM reply from non-owner
1279 return 'reply'
1280 return 'waiting'
1281
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001282 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001283 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001284
1285 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001286 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001287 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001288 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001289 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001290 except presubmit_support.PresubmitFailure, e:
1291 DieWithError(
1292 ('%s\nMaybe your depot_tools is out of date?\n'
1293 'If all fails, contact maruel@') % e)
1294
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001295 def UpdateDescription(self, description):
1296 self.description = description
1297 return self.RpcServer().update_description(
1298 self.GetIssue(), self.description)
1299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001301 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001302 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001304 def SetFlag(self, flag, value):
1305 """Patchset must match."""
1306 if not self.GetPatchset():
1307 DieWithError('The patchset needs to match. Send another patchset.')
1308 try:
1309 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001310 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001311 except urllib2.HTTPError, e:
1312 if e.code == 404:
1313 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1314 if e.code == 403:
1315 DieWithError(
1316 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1317 'match?') % (self.GetIssue(), self.GetPatchset()))
1318 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001320 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 """Returns an upload.RpcServer() to access this review's rietveld instance.
1322 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001323 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001324 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001325 self.GetRietveldServer(),
1326 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001327 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328
1329 def _IssueSetting(self):
1330 """Return the git setting that stores this change's issue."""
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001331 if settings.GetIsGerrit():
1332 return 'branch.%s.gerritissue' % self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333 return 'branch.%s.rietveldissue' % self.GetBranch()
1334
1335 def _PatchsetSetting(self):
1336 """Return the git setting that stores this change's most recent patchset."""
1337 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1338
1339 def _RietveldServer(self):
1340 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001341 branch = self.GetBranch()
1342 if branch:
1343 return 'branch.%s.rietveldserver' % branch
1344 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345
1346
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001347class ChangeDescription(object):
1348 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001349 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001350 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001351
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001352 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001353 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001354
agable@chromium.org42c20792013-09-12 17:34:49 +00001355 @property # www.logilab.org/ticket/89786
1356 def description(self): # pylint: disable=E0202
1357 return '\n'.join(self._description_lines)
1358
1359 def set_description(self, desc):
1360 if isinstance(desc, basestring):
1361 lines = desc.splitlines()
1362 else:
1363 lines = [line.rstrip() for line in desc]
1364 while lines and not lines[0]:
1365 lines.pop(0)
1366 while lines and not lines[-1]:
1367 lines.pop(-1)
1368 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001369
piman@chromium.org336f9122014-09-04 02:16:55 +00001370 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001373 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001374 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001375 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001376
agable@chromium.org42c20792013-09-12 17:34:49 +00001377 # Get the set of R= and TBR= lines and remove them from the desciption.
1378 regexp = re.compile(self.R_LINE)
1379 matches = [regexp.match(line) for line in self._description_lines]
1380 new_desc = [l for i, l in enumerate(self._description_lines)
1381 if not matches[i]]
1382 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001383
agable@chromium.org42c20792013-09-12 17:34:49 +00001384 # Construct new unified R= and TBR= lines.
1385 r_names = []
1386 tbr_names = []
1387 for match in matches:
1388 if not match:
1389 continue
1390 people = cleanup_list([match.group(2).strip()])
1391 if match.group(1) == 'TBR':
1392 tbr_names.extend(people)
1393 else:
1394 r_names.extend(people)
1395 for name in r_names:
1396 if name not in reviewers:
1397 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001398 if add_owners_tbr:
1399 owners_db = owners.Database(change.RepositoryRoot(),
1400 fopen=file, os_path=os.path, glob=glob.glob)
1401 all_reviewers = set(tbr_names + reviewers)
1402 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1403 all_reviewers)
1404 tbr_names.extend(owners_db.reviewers_for(missing_files,
1405 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001406 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1407 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1408
1409 # Put the new lines in the description where the old first R= line was.
1410 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1411 if 0 <= line_loc < len(self._description_lines):
1412 if new_tbr_line:
1413 self._description_lines.insert(line_loc, new_tbr_line)
1414 if new_r_line:
1415 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001416 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001417 if new_r_line:
1418 self.append_footer(new_r_line)
1419 if new_tbr_line:
1420 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001421
1422 def prompt(self):
1423 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 self.set_description([
1425 '# Enter a description of the change.',
1426 '# This will be displayed on the codereview site.',
1427 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001428 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001429 '--------------------',
1430 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001431
agable@chromium.org42c20792013-09-12 17:34:49 +00001432 regexp = re.compile(self.BUG_LINE)
1433 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001434 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001435 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001436 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001437 if not content:
1438 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001439 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001440
1441 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001442 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1443 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001444 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001445 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001446
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001447 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001448 if self._description_lines:
1449 # Add an empty line if either the last line or the new line isn't a tag.
1450 last_line = self._description_lines[-1]
1451 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1452 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1453 self._description_lines.append('')
1454 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001455
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001456 def get_reviewers(self):
1457 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001458 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1459 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001460 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001461
1462
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001463def get_approving_reviewers(props):
1464 """Retrieves the reviewers that approved a CL from the issue properties with
1465 messages.
1466
1467 Note that the list may contain reviewers that are not committer, thus are not
1468 considered by the CQ.
1469 """
1470 return sorted(
1471 set(
1472 message['sender']
1473 for message in props['messages']
1474 if message['approval'] and message['sender'] in props['reviewers']
1475 )
1476 )
1477
1478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479def FindCodereviewSettingsFile(filename='codereview.settings'):
1480 """Finds the given file starting in the cwd and going up.
1481
1482 Only looks up to the top of the repository unless an
1483 'inherit-review-settings-ok' file exists in the root of the repository.
1484 """
1485 inherit_ok_file = 'inherit-review-settings-ok'
1486 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001487 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1489 root = '/'
1490 while True:
1491 if filename in os.listdir(cwd):
1492 if os.path.isfile(os.path.join(cwd, filename)):
1493 return open(os.path.join(cwd, filename))
1494 if cwd == root:
1495 break
1496 cwd = os.path.dirname(cwd)
1497
1498
1499def LoadCodereviewSettingsFromFile(fileobj):
1500 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001501 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 def SetProperty(name, setting, unset_error_ok=False):
1504 fullname = 'rietveld.' + name
1505 if setting in keyvals:
1506 RunGit(['config', fullname, keyvals[setting]])
1507 else:
1508 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1509
1510 SetProperty('server', 'CODE_REVIEW_SERVER')
1511 # Only server setting is required. Other settings can be absent.
1512 # In that case, we ignore errors raised during option deletion attempt.
1513 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001514 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1516 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001517 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001518 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001519 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1520 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001521 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001522 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001523 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001524 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1525 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001527 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001528 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001529
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001530 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1531 RunGit(['config', 'gerrit.squash-uploads',
1532 keyvals['GERRIT_SQUASH_UPLOADS']])
1533
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1535 #should be of the form
1536 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1537 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1538 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1539 keyvals['ORIGIN_URL_CONFIG']])
1540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001542def urlretrieve(source, destination):
1543 """urllib is broken for SSL connections via a proxy therefore we
1544 can't use urllib.urlretrieve()."""
1545 with open(destination, 'w') as f:
1546 f.write(urllib2.urlopen(source).read())
1547
1548
ukai@chromium.org712d6102013-11-27 00:52:58 +00001549def hasSheBang(fname):
1550 """Checks fname is a #! script."""
1551 with open(fname) as f:
1552 return f.read(2).startswith('#!')
1553
1554
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001555def DownloadGerritHook(force):
1556 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001557
1558 Args:
1559 force: True to update hooks. False to install hooks if not present.
1560 """
1561 if not settings.GetIsGerrit():
1562 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001563 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001564 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1565 if not os.access(dst, os.X_OK):
1566 if os.path.exists(dst):
1567 if not force:
1568 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001569 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001570 print(
1571 'WARNING: installing Gerrit commit-msg hook.\n'
1572 ' This behavior of git cl will soon be disabled.\n'
1573 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001574 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001575 if not hasSheBang(dst):
1576 DieWithError('Not a script: %s\n'
1577 'You need to download from\n%s\n'
1578 'into .git/hooks/commit-msg and '
1579 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001580 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1581 except Exception:
1582 if os.path.exists(dst):
1583 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001584 DieWithError('\nFailed to download hooks.\n'
1585 'You need to download from\n%s\n'
1586 'into .git/hooks/commit-msg and '
1587 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001588
1589
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001590
1591def GetRietveldCodereviewSettingsInteractively():
1592 """Prompt the user for settings."""
1593 server = settings.GetDefaultServerUrl(error_ok=True)
1594 prompt = 'Rietveld server (host[:port])'
1595 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1596 newserver = ask_for_data(prompt + ':')
1597 if not server and not newserver:
1598 newserver = DEFAULT_SERVER
1599 if newserver:
1600 newserver = gclient_utils.UpgradeToHttps(newserver)
1601 if newserver != server:
1602 RunGit(['config', 'rietveld.server', newserver])
1603
1604 def SetProperty(initial, caption, name, is_url):
1605 prompt = caption
1606 if initial:
1607 prompt += ' ("x" to clear) [%s]' % initial
1608 new_val = ask_for_data(prompt + ':')
1609 if new_val == 'x':
1610 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1611 elif new_val:
1612 if is_url:
1613 new_val = gclient_utils.UpgradeToHttps(new_val)
1614 if new_val != initial:
1615 RunGit(['config', 'rietveld.' + name, new_val])
1616
1617 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1618 SetProperty(settings.GetDefaultPrivateFlag(),
1619 'Private flag (rietveld only)', 'private', False)
1620 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1621 'tree-status-url', False)
1622 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1623 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1624 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1625 'run-post-upload-hook', False)
1626
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001627@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001629 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001631 print('WARNING: git cl config works for Rietveld only.\n'
1632 'For Gerrit, see http://crbug.com/579160.')
1633 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001634 parser.add_option('--activate-update', action='store_true',
1635 help='activate auto-updating [rietveld] section in '
1636 '.git/config')
1637 parser.add_option('--deactivate-update', action='store_true',
1638 help='deactivate auto-updating [rietveld] section in '
1639 '.git/config')
1640 options, args = parser.parse_args(args)
1641
1642 if options.deactivate_update:
1643 RunGit(['config', 'rietveld.autoupdate', 'false'])
1644 return
1645
1646 if options.activate_update:
1647 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1648 return
1649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001651 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 return 0
1653
1654 url = args[0]
1655 if not url.endswith('codereview.settings'):
1656 url = os.path.join(url, 'codereview.settings')
1657
1658 # Load code review settings and download hooks (if available).
1659 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1660 return 0
1661
1662
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001663def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001664 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001665 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1666 branch = ShortBranchName(branchref)
1667 _, args = parser.parse_args(args)
1668 if not args:
1669 print("Current base-url:")
1670 return RunGit(['config', 'branch.%s.base-url' % branch],
1671 error_ok=False).strip()
1672 else:
1673 print("Setting base-url to %s" % args[0])
1674 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1675 error_ok=False).strip()
1676
1677
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001678def color_for_status(status):
1679 """Maps a Changelist status to color, for CMDstatus and other tools."""
1680 return {
1681 'unsent': Fore.RED,
1682 'waiting': Fore.BLUE,
1683 'reply': Fore.YELLOW,
1684 'lgtm': Fore.GREEN,
1685 'commit': Fore.MAGENTA,
1686 'closed': Fore.CYAN,
1687 'error': Fore.WHITE,
1688 }.get(status, Fore.WHITE)
1689
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001690def fetch_cl_status(branch, auth_config=None):
1691 """Fetches information for an issue and returns (branch, issue, status)."""
1692 cl = Changelist(branchref=branch, auth_config=auth_config)
1693 url = cl.GetIssueURL()
1694 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001695
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001696 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001697 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001698 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001699
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001700 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001701
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001702def get_cl_statuses(
1703 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001704 """Returns a blocking iterable of (branch, issue, color) for given branches.
1705
1706 If fine_grained is true, this will fetch CL statuses from the server.
1707 Otherwise, simply indicate if there's a matching url for the given branches.
1708
1709 If max_processes is specified, it is used as the maximum number of processes
1710 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1711 spawned.
1712 """
1713 # Silence upload.py otherwise it becomes unwieldly.
1714 upload.verbosity = 0
1715
1716 if fine_grained:
1717 # Process one branch synchronously to work through authentication, then
1718 # spawn processes to process all the other branches in parallel.
1719 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001720 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1721 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001722
1723 branches_to_fetch = branches[1:]
1724 pool = ThreadPool(
1725 min(max_processes, len(branches_to_fetch))
1726 if max_processes is not None
1727 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001728 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001729 yield x
1730 else:
1731 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1732 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001733 cl = Changelist(branchref=b, auth_config=auth_config)
1734 url = cl.GetIssueURL()
1735 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001736
rmistry@google.com2dd99862015-06-22 12:22:18 +00001737
1738def upload_branch_deps(cl, args):
1739 """Uploads CLs of local branches that are dependents of the current branch.
1740
1741 If the local branch dependency tree looks like:
1742 test1 -> test2.1 -> test3.1
1743 -> test3.2
1744 -> test2.2 -> test3.3
1745
1746 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1747 run on the dependent branches in this order:
1748 test2.1, test3.1, test3.2, test2.2, test3.3
1749
1750 Note: This function does not rebase your local dependent branches. Use it when
1751 you make a change to the parent branch that will not conflict with its
1752 dependent branches, and you would like their dependencies updated in
1753 Rietveld.
1754 """
1755 if git_common.is_dirty_git_tree('upload-branch-deps'):
1756 return 1
1757
1758 root_branch = cl.GetBranch()
1759 if root_branch is None:
1760 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1761 'Get on a branch!')
1762 if not cl.GetIssue() or not cl.GetPatchset():
1763 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1764 'patchset dependencies without an uploaded CL.')
1765
1766 branches = RunGit(['for-each-ref',
1767 '--format=%(refname:short) %(upstream:short)',
1768 'refs/heads'])
1769 if not branches:
1770 print('No local branches found.')
1771 return 0
1772
1773 # Create a dictionary of all local branches to the branches that are dependent
1774 # on it.
1775 tracked_to_dependents = collections.defaultdict(list)
1776 for b in branches.splitlines():
1777 tokens = b.split()
1778 if len(tokens) == 2:
1779 branch_name, tracked = tokens
1780 tracked_to_dependents[tracked].append(branch_name)
1781
1782 print
1783 print 'The dependent local branches of %s are:' % root_branch
1784 dependents = []
1785 def traverse_dependents_preorder(branch, padding=''):
1786 dependents_to_process = tracked_to_dependents.get(branch, [])
1787 padding += ' '
1788 for dependent in dependents_to_process:
1789 print '%s%s' % (padding, dependent)
1790 dependents.append(dependent)
1791 traverse_dependents_preorder(dependent, padding)
1792 traverse_dependents_preorder(root_branch)
1793 print
1794
1795 if not dependents:
1796 print 'There are no dependent local branches for %s' % root_branch
1797 return 0
1798
1799 print ('This command will checkout all dependent branches and run '
1800 '"git cl upload".')
1801 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1802
andybons@chromium.org962f9462016-02-03 20:00:42 +00001803 # Add a default patchset title to all upload calls in Rietveld.
1804 if not settings.GetIsGerrit():
1805 args.extend(['-t', 'Updated patchset dependency'])
1806
rmistry@google.com2dd99862015-06-22 12:22:18 +00001807 # Record all dependents that failed to upload.
1808 failures = {}
1809 # Go through all dependents, checkout the branch and upload.
1810 try:
1811 for dependent_branch in dependents:
1812 print
1813 print '--------------------------------------'
1814 print 'Running "git cl upload" from %s:' % dependent_branch
1815 RunGit(['checkout', '-q', dependent_branch])
1816 print
1817 try:
1818 if CMDupload(OptionParser(), args) != 0:
1819 print 'Upload failed for %s!' % dependent_branch
1820 failures[dependent_branch] = 1
1821 except: # pylint: disable=W0702
1822 failures[dependent_branch] = 1
1823 print
1824 finally:
1825 # Swap back to the original root branch.
1826 RunGit(['checkout', '-q', root_branch])
1827
1828 print
1829 print 'Upload complete for dependent branches!'
1830 for dependent_branch in dependents:
1831 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1832 print ' %s : %s' % (dependent_branch, upload_status)
1833 print
1834
1835 return 0
1836
1837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001839 """Show status of changelists.
1840
1841 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001842 - Red not sent for review or broken
1843 - Blue waiting for review
1844 - Yellow waiting for you to reply to review
1845 - Green LGTM'ed
1846 - Magenta in the commit queue
1847 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001848
1849 Also see 'git cl comments'.
1850 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 parser.add_option('--field',
1852 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001853 parser.add_option('-f', '--fast', action='store_true',
1854 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001855 parser.add_option(
1856 '-j', '--maxjobs', action='store', type=int,
1857 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001858
1859 auth.add_auth_options(parser)
1860 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001861 if args:
1862 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001863 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001866 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 if options.field.startswith('desc'):
1868 print cl.GetDescription()
1869 elif options.field == 'id':
1870 issueid = cl.GetIssue()
1871 if issueid:
1872 print issueid
1873 elif options.field == 'patch':
1874 patchset = cl.GetPatchset()
1875 if patchset:
1876 print patchset
1877 elif options.field == 'url':
1878 url = cl.GetIssueURL()
1879 if url:
1880 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001881 return 0
1882
1883 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1884 if not branches:
1885 print('No local branch found.')
1886 return 0
1887
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001888 changes = (
1889 Changelist(branchref=b, auth_config=auth_config)
1890 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001891 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001892 alignment = max(5, max(len(b) for b in branches))
1893 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001894 output = get_cl_statuses(branches,
1895 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001896 max_processes=options.maxjobs,
1897 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001898
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001899 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001900 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001901 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001902 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001903 b, i, status = output.next()
1904 branch_statuses[b] = (i, status)
1905 issue_url, status = branch_statuses.pop(branch)
1906 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001907 reset = Fore.RESET
1908 if not sys.stdout.isatty():
1909 color = ''
1910 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001911 status_str = '(%s)' % status if status else ''
1912 print ' %*s : %s%s %s%s' % (
1913 alignment, ShortBranchName(branch), color, issue_url, status_str,
1914 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001916 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001917 print
1918 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001919 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001920 if not cl.GetIssue():
1921 print 'No issue assigned.'
1922 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001923 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001924 if not options.fast:
1925 print 'Issue description:'
1926 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 return 0
1928
1929
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001930def colorize_CMDstatus_doc():
1931 """To be called once in main() to add colors to git cl status help."""
1932 colors = [i for i in dir(Fore) if i[0].isupper()]
1933
1934 def colorize_line(line):
1935 for color in colors:
1936 if color in line.upper():
1937 # Extract whitespaces first and the leading '-'.
1938 indent = len(line) - len(line.lstrip(' ')) + 1
1939 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1940 return line
1941
1942 lines = CMDstatus.__doc__.splitlines()
1943 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1944
1945
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001946@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001948 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949
1950 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001951 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001952 parser.add_option('-r', '--reverse', action='store_true',
1953 help='Lookup the branch(es) for the specified issues. If '
1954 'no issues are specified, all branches with mapped '
1955 'issues will be listed.')
1956 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957
dnj@chromium.org406c4402015-03-03 17:22:28 +00001958 if options.reverse:
1959 branches = RunGit(['for-each-ref', 'refs/heads',
1960 '--format=%(refname:short)']).splitlines()
1961
1962 # Reverse issue lookup.
1963 issue_branch_map = {}
1964 for branch in branches:
1965 cl = Changelist(branchref=branch)
1966 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1967 if not args:
1968 args = sorted(issue_branch_map.iterkeys())
1969 for issue in args:
1970 if not issue:
1971 continue
1972 print 'Branch for issue number %s: %s' % (
1973 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1974 else:
1975 cl = Changelist()
1976 if len(args) > 0:
1977 try:
1978 issue = int(args[0])
1979 except ValueError:
1980 DieWithError('Pass a number to set the issue or none to list it.\n'
1981 'Maybe you want to run git cl status?')
1982 cl.SetIssue(issue)
1983 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984 return 0
1985
1986
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001987def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001988 """Shows or posts review comments for any changelist."""
1989 parser.add_option('-a', '--add-comment', dest='comment',
1990 help='comment to add to an issue')
1991 parser.add_option('-i', dest='issue',
1992 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001993 parser.add_option('-j', '--json-file',
1994 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001995 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001996 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001997 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001998
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001999 issue = None
2000 if options.issue:
2001 try:
2002 issue = int(options.issue)
2003 except ValueError:
2004 DieWithError('A review issue id is expected to be a number')
2005
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002006 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002007
2008 if options.comment:
2009 cl.AddComment(options.comment)
2010 return 0
2011
2012 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002013 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002014 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002015 summary.append({
2016 'date': message['date'],
2017 'lgtm': False,
2018 'message': message['text'],
2019 'not_lgtm': False,
2020 'sender': message['sender'],
2021 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002022 if message['disapproval']:
2023 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002024 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002025 elif message['approval']:
2026 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002027 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002028 elif message['sender'] == data['owner_email']:
2029 color = Fore.MAGENTA
2030 else:
2031 color = Fore.BLUE
2032 print '\n%s%s %s%s' % (
2033 color, message['date'].split('.', 1)[0], message['sender'],
2034 Fore.RESET)
2035 if message['text'].strip():
2036 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002037 if options.json_file:
2038 with open(options.json_file, 'wb') as f:
2039 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002040 return 0
2041
2042
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002043def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002044 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002045 parser.add_option('-d', '--display', action='store_true',
2046 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002047 auth.add_auth_options(parser)
2048 options, _ = parser.parse_args(args)
2049 auth_config = auth.extract_auth_config_from_options(options)
2050 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002051 if not cl.GetIssue():
2052 DieWithError('This branch has no associated changelist.')
2053 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002054 if options.display:
2055 print description.description
2056 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002057 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002058 if cl.GetDescription() != description.description:
2059 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002060 return 0
2061
2062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063def CreateDescriptionFromLog(args):
2064 """Pulls out the commit log to use as a base for the CL description."""
2065 log_args = []
2066 if len(args) == 1 and not args[0].endswith('.'):
2067 log_args = [args[0] + '..']
2068 elif len(args) == 1 and args[0].endswith('...'):
2069 log_args = [args[0][:-1]]
2070 elif len(args) == 2:
2071 log_args = [args[0] + '..' + args[1]]
2072 else:
2073 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002074 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
2076
thestig@chromium.org44202a22014-03-11 19:22:18 +00002077def CMDlint(parser, args):
2078 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002079 parser.add_option('--filter', action='append', metavar='-x,+y',
2080 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002081 auth.add_auth_options(parser)
2082 options, args = parser.parse_args(args)
2083 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002084
2085 # Access to a protected member _XX of a client class
2086 # pylint: disable=W0212
2087 try:
2088 import cpplint
2089 import cpplint_chromium
2090 except ImportError:
2091 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2092 return 1
2093
2094 # Change the current working directory before calling lint so that it
2095 # shows the correct base.
2096 previous_cwd = os.getcwd()
2097 os.chdir(settings.GetRoot())
2098 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002099 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002100 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2101 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002102 if not files:
2103 print "Cannot lint an empty CL"
2104 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002105
2106 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002107 command = args + files
2108 if options.filter:
2109 command = ['--filter=' + ','.join(options.filter)] + command
2110 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002111
2112 white_regex = re.compile(settings.GetLintRegex())
2113 black_regex = re.compile(settings.GetLintIgnoreRegex())
2114 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2115 for filename in filenames:
2116 if white_regex.match(filename):
2117 if black_regex.match(filename):
2118 print "Ignoring file %s" % filename
2119 else:
2120 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2121 extra_check_functions)
2122 else:
2123 print "Skipping file %s" % filename
2124 finally:
2125 os.chdir(previous_cwd)
2126 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2127 if cpplint._cpplint_state.error_count != 0:
2128 return 1
2129 return 0
2130
2131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002132def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002133 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002134 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002136 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002137 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002138 auth.add_auth_options(parser)
2139 options, args = parser.parse_args(args)
2140 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141
sbc@chromium.org71437c02015-04-09 19:29:40 +00002142 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002143 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144 return 1
2145
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002146 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147 if args:
2148 base_branch = args[0]
2149 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002150 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002151 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002153 cl.RunHook(
2154 committing=not options.upload,
2155 may_prompt=False,
2156 verbose=options.verbose,
2157 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002158 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159
2160
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002161def AddChangeIdToCommitMessage(options, args):
2162 """Re-commits using the current message, assumes the commit hook is in
2163 place.
2164 """
2165 log_desc = options.message or CreateDescriptionFromLog(args)
2166 git_command = ['commit', '--amend', '-m', log_desc]
2167 RunGit(git_command)
2168 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002169 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002170 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002171 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002172 else:
2173 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2174
2175
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002176def GenerateGerritChangeId(message):
2177 """Returns Ixxxxxx...xxx change id.
2178
2179 Works the same way as
2180 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2181 but can be called on demand on all platforms.
2182
2183 The basic idea is to generate git hash of a state of the tree, original commit
2184 message, author/committer info and timestamps.
2185 """
2186 lines = []
2187 tree_hash = RunGitSilent(['write-tree'])
2188 lines.append('tree %s' % tree_hash.strip())
2189 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2190 if code == 0:
2191 lines.append('parent %s' % parent.strip())
2192 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2193 lines.append('author %s' % author.strip())
2194 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2195 lines.append('committer %s' % committer.strip())
2196 lines.append('')
2197 # Note: Gerrit's commit-hook actually cleans message of some lines and
2198 # whitespace. This code is not doing this, but it clearly won't decrease
2199 # entropy.
2200 lines.append(message)
2201 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2202 stdin='\n'.join(lines))
2203 return 'I%s' % change_hash.strip()
2204
2205
piman@chromium.org336f9122014-09-04 02:16:55 +00002206def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002207 """upload the current branch to gerrit."""
2208 # We assume the remote called "origin" is the one we want.
2209 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002210 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002211
2212 remote, remote_branch = cl.GetRemoteBranch()
2213 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2214 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002216 change_desc = ChangeDescription(
2217 options.message or CreateDescriptionFromLog(args))
2218 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002219 print "\nDescription is empty. Aborting..."
2220 return 1
2221
2222 if options.title:
2223 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002224 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002225
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002226 if options.squash:
2227 # Try to get the message from a previous upload.
2228 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002229 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002230 if not message:
2231 if not options.force:
2232 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002233 if not change_desc.description:
2234 print "Description is empty; aborting."
2235 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002236 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002237 change_ids = git_footers.get_footer_change_id(message)
2238 if len(change_ids) > 1:
2239 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2240 if not change_ids:
2241 message = git_footers.add_footer_change_id(
2242 message, GenerateGerritChangeId(message))
2243 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002244 change_ids = git_footers.get_footer_change_id(message)
2245 assert len(change_ids) == 1
2246
2247 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002248
2249 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2250 if remote is '.':
2251 # If our upstream branch is local, we base our squashed commit on its
2252 # squashed version.
2253 parent = ('refs/heads/git_cl_uploads/' +
2254 scm.GIT.ShortBranchName(upstream_branch))
2255
2256 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2257 # will create additional CLs when uploading.
2258 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2259 RunGitSilent(['rev-parse', parent + ':'])):
2260 print 'Upload upstream branch ' + upstream_branch + ' first.'
2261 return 1
2262 else:
2263 parent = cl.GetCommonAncestorWithUpstream()
2264
2265 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2266 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2267 '-m', message]).strip()
2268 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002269 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002270 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002271 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002272 ref_to_push = 'HEAD'
2273 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002274 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002275
2276 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2277 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002278 if len(commits) > 1:
2279 print('WARNING: This will upload %d commits. Run the following command '
2280 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002281 print('git log %s..%s' % (parent, ref_to_push))
2282 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002283 'commit.')
2284 ask_for_data('About to upload; enter to confirm.')
2285
piman@chromium.org336f9122014-09-04 02:16:55 +00002286 if options.reviewers or options.tbr_owners:
2287 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002288
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 receive_options = []
2290 cc = cl.GetCCList().split(',')
2291 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002292 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002293 cc = filter(None, cc)
2294 if cc:
2295 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002296 if change_desc.get_reviewers():
2297 receive_options.extend(
2298 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299
ukai@chromium.orge8077812012-02-03 03:41:46 +00002300 git_command = ['push']
2301 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002302 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002303 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002304 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002305 push_stdout = gclient_utils.CheckCallAndFilter(
2306 ['git'] + git_command,
2307 print_stdout=True,
2308 # Flush after every line: useful for seeing progress when running as
2309 # recipe.
2310 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002311
2312 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002313 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2314 change_numbers = [m.group(1)
2315 for m in map(regex.match, push_stdout.splitlines())
2316 if m]
2317 if len(change_numbers) != 1:
2318 DieWithError(
2319 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2320 'Change-Id: %s') % (len(change_numbers), change_id))
2321 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002322 head = RunGit(['rev-parse', 'HEAD']).strip()
2323 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002324 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002326
wittman@chromium.org455dc922015-01-26 20:15:50 +00002327def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2328 """Computes the remote branch ref to use for the CL.
2329
2330 Args:
2331 remote (str): The git remote for the CL.
2332 remote_branch (str): The git remote branch for the CL.
2333 target_branch (str): The target branch specified by the user.
2334 pending_prefix (str): The pending prefix from the settings.
2335 """
2336 if not (remote and remote_branch):
2337 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002338
wittman@chromium.org455dc922015-01-26 20:15:50 +00002339 if target_branch:
2340 # Cannonicalize branch references to the equivalent local full symbolic
2341 # refs, which are then translated into the remote full symbolic refs
2342 # below.
2343 if '/' not in target_branch:
2344 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2345 else:
2346 prefix_replacements = (
2347 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2348 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2349 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2350 )
2351 match = None
2352 for regex, replacement in prefix_replacements:
2353 match = re.search(regex, target_branch)
2354 if match:
2355 remote_branch = target_branch.replace(match.group(0), replacement)
2356 break
2357 if not match:
2358 # This is a branch path but not one we recognize; use as-is.
2359 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002360 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2361 # Handle the refs that need to land in different refs.
2362 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002363
wittman@chromium.org455dc922015-01-26 20:15:50 +00002364 # Create the true path to the remote branch.
2365 # Does the following translation:
2366 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2367 # * refs/remotes/origin/master -> refs/heads/master
2368 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2369 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2370 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2371 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2372 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2373 'refs/heads/')
2374 elif remote_branch.startswith('refs/remotes/branch-heads'):
2375 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2376 # If a pending prefix exists then replace refs/ with it.
2377 if pending_prefix:
2378 remote_branch = remote_branch.replace('refs/', pending_prefix)
2379 return remote_branch
2380
2381
piman@chromium.org336f9122014-09-04 02:16:55 +00002382def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002383 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002384 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2385 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002386 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387 if options.emulate_svn_auto_props:
2388 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389
2390 change_desc = None
2391
pgervais@chromium.org91141372014-01-09 23:27:20 +00002392 if options.email is not None:
2393 upload_args.extend(['--email', options.email])
2394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002396 if options.title:
2397 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002398 if options.message:
2399 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002400 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002401 print ("This branch is associated with issue %s. "
2402 "Adding patch to that issue." % cl.GetIssue())
2403 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002404 if options.title:
2405 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002406 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002407 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002408 if options.reviewers or options.tbr_owners:
2409 change_desc.update_reviewers(options.reviewers,
2410 options.tbr_owners,
2411 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002412 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002413 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002414
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002415 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002416 print "Description is empty; aborting."
2417 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002418
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002419 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002420 if change_desc.get_reviewers():
2421 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002422 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002423 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002424 DieWithError("Must specify reviewers to send email.")
2425 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002426
2427 # We check this before applying rietveld.private assuming that in
2428 # rietveld.cc only addresses which we can send private CLs to are listed
2429 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2430 # --private is specified explicitly on the command line.
2431 if options.private:
2432 logging.warn('rietveld.cc is ignored since private flag is specified. '
2433 'You need to review and add them manually if necessary.')
2434 cc = cl.GetCCListWithoutDefault()
2435 else:
2436 cc = cl.GetCCList()
2437 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002438 if cc:
2439 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002441 if options.private or settings.GetDefaultPrivateFlag() == "True":
2442 upload_args.append('--private')
2443
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002444 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002445 if not options.find_copies:
2446 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002448 # Include the upstream repo's URL in the change -- this is useful for
2449 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002450 remote_url = cl.GetGitBaseUrlFromConfig()
2451 if not remote_url:
2452 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002453 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002454 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002455 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2456 remote_url = (cl.GetRemoteUrl() + '@'
2457 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002458 if remote_url:
2459 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002460 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002461 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2462 settings.GetPendingRefPrefix())
2463 if target_ref:
2464 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002465
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002466 # Look for dependent patchsets. See crbug.com/480453 for more details.
2467 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2468 upstream_branch = ShortBranchName(upstream_branch)
2469 if remote is '.':
2470 # A local branch is being tracked.
2471 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002472 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002473 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002474 print ('Skipping dependency patchset upload because git config '
2475 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002476 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002477 else:
2478 auth_config = auth.extract_auth_config_from_options(options)
2479 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2480 branch_cl_issue_url = branch_cl.GetIssueURL()
2481 branch_cl_issue = branch_cl.GetIssue()
2482 branch_cl_patchset = branch_cl.GetPatchset()
2483 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2484 upload_args.extend(
2485 ['--depends_on_patchset', '%s:%s' % (
2486 branch_cl_issue, branch_cl_patchset)])
2487 print
2488 print ('The current branch (%s) is tracking a local branch (%s) with '
2489 'an associated CL.') % (cl.GetBranch(), local_branch)
2490 print 'Adding %s/#ps%s as a dependency patchset.' % (
2491 branch_cl_issue_url, branch_cl_patchset)
2492 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002493
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002494 project = settings.GetProject()
2495 if project:
2496 upload_args.extend(['--project', project])
2497
rmistry@google.comef966222015-04-07 11:15:01 +00002498 if options.cq_dry_run:
2499 upload_args.extend(['--cq_dry_run'])
2500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002501 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002502 upload_args = ['upload'] + upload_args + args
2503 logging.info('upload.RealMain(%s)', upload_args)
2504 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002505 issue = int(issue)
2506 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002507 except KeyboardInterrupt:
2508 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509 except:
2510 # If we got an exception after the user typed a description for their
2511 # change, back up the description before re-raising.
2512 if change_desc:
2513 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2514 print '\nGot exception while uploading -- saving description to %s\n' \
2515 % backup_path
2516 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002517 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518 backup_file.close()
2519 raise
2520
2521 if not cl.GetIssue():
2522 cl.SetIssue(issue)
2523 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002524
2525 if options.use_commit_queue:
2526 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002527 return 0
2528
2529
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002530def cleanup_list(l):
2531 """Fixes a list so that comma separated items are put as individual items.
2532
2533 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2534 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2535 """
2536 items = sum((i.split(',') for i in l), [])
2537 stripped_items = (i.strip() for i in items)
2538 return sorted(filter(None, stripped_items))
2539
2540
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002541@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002542def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002543 """Uploads the current changelist to codereview.
2544
2545 Can skip dependency patchset uploads for a branch by running:
2546 git config branch.branch_name.skip-deps-uploads True
2547 To unset run:
2548 git config --unset branch.branch_name.skip-deps-uploads
2549 Can also set the above globally by using the --global flag.
2550 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002551 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2552 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002553 parser.add_option('--bypass-watchlists', action='store_true',
2554 dest='bypass_watchlists',
2555 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002556 parser.add_option('-f', action='store_true', dest='force',
2557 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002558 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002559 parser.add_option('-t', dest='title',
2560 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002561 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002562 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002563 help='reviewer email addresses')
2564 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002565 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002566 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002567 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002568 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002569 parser.add_option('--emulate_svn_auto_props',
2570 '--emulate-svn-auto-props',
2571 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002572 dest="emulate_svn_auto_props",
2573 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002574 parser.add_option('-c', '--use-commit-queue', action='store_true',
2575 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002576 parser.add_option('--private', action='store_true',
2577 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002578 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002579 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002580 metavar='TARGET',
2581 help='Apply CL to remote ref TARGET. ' +
2582 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002583 parser.add_option('--squash', action='store_true',
2584 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002585 parser.add_option('--no-squash', action='store_true',
2586 help='Don\'t squash multiple commits into one ' +
2587 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002588 parser.add_option('--email', default=None,
2589 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002590 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2591 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002592 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2593 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002594 help='Send the patchset to do a CQ dry run right after '
2595 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002596 parser.add_option('--dependencies', action='store_true',
2597 help='Uploads CLs of all the local branches that depend on '
2598 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002599
rmistry@google.com2dd99862015-06-22 12:22:18 +00002600 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002601 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002602 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002603 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002604 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002605
sbc@chromium.org71437c02015-04-09 19:29:40 +00002606 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002607 return 1
2608
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002609 options.reviewers = cleanup_list(options.reviewers)
2610 options.cc = cleanup_list(options.cc)
2611
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002612 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002613 if args:
2614 # TODO(ukai): is it ok for gerrit case?
2615 base_branch = args[0]
2616 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002617 if cl.GetBranch() is None:
2618 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2619
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002620 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002621 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002622 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002623
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002624 # Make sure authenticated to Rietveld before running expensive hooks. It is
2625 # a fast, best efforts check. Rietveld still can reject the authentication
2626 # during the actual upload.
2627 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2628 authenticator = auth.get_authenticator_for_host(
2629 cl.GetRietveldServer(), auth_config)
2630 if not authenticator.has_cached_credentials():
2631 raise auth.LoginRequiredError(cl.GetRietveldServer())
2632
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002633 # Apply watchlists on upload.
2634 change = cl.GetChange(base_branch, None)
2635 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2636 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002637 if not options.bypass_watchlists:
2638 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002639
ukai@chromium.orge8077812012-02-03 03:41:46 +00002640 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002641 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002642 # Set the reviewer list now so that presubmit checks can access it.
2643 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002644 change_description.update_reviewers(options.reviewers,
2645 options.tbr_owners,
2646 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002647 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002648 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002649 may_prompt=not options.force,
2650 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002651 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002652 if not hook_results.should_continue():
2653 return 1
2654 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002655 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002656
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002657 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002658 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002659 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002660 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002661 print ('The last upload made from this repository was patchset #%d but '
2662 'the most recent patchset on the server is #%d.'
2663 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002664 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2665 'from another machine or branch the patch you\'re uploading now '
2666 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002667 ask_for_data('About to upload; enter to confirm.')
2668
iannucci@chromium.org79540052012-10-19 23:15:26 +00002669 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002670 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002671 if options.squash and options.no_squash:
2672 DieWithError('Can only use one of --squash or --no-squash')
2673
2674 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2675 not options.no_squash)
2676
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002677 ret = GerritUpload(options, args, cl, change)
2678 else:
2679 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002680 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002681 git_set_branch_value('last-upload-hash',
2682 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002683 # Run post upload hooks, if specified.
2684 if settings.GetRunPostUploadHook():
2685 presubmit_support.DoPostUploadExecuter(
2686 change,
2687 cl,
2688 settings.GetRoot(),
2689 options.verbose,
2690 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002691
rmistry@google.com2dd99862015-06-22 12:22:18 +00002692 # Upload all dependencies if specified.
2693 if options.dependencies:
2694 print
2695 print '--dependencies has been specified.'
2696 print 'All dependent local branches will be re-uploaded.'
2697 print
2698 # Remove the dependencies flag from args so that we do not end up in a
2699 # loop.
2700 orig_args.remove('--dependencies')
2701 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002702 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002703
2704
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002705def IsSubmoduleMergeCommit(ref):
2706 # When submodules are added to the repo, we expect there to be a single
2707 # non-git-svn merge commit at remote HEAD with a signature comment.
2708 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002709 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002710 return RunGit(cmd) != ''
2711
2712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002714 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002716 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 Updates changelog with metadata (e.g. pointer to review).
2718 Pushes/dcommits the code upstream.
2719 Updates review and closes.
2720 """
2721 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2722 help='bypass upload presubmit hook')
2723 parser.add_option('-m', dest='message',
2724 help="override review description")
2725 parser.add_option('-f', action='store_true', dest='force',
2726 help="force yes to questions (don't prompt)")
2727 parser.add_option('-c', dest='contributor',
2728 help="external contributor for patch (appended to " +
2729 "description and used as author for git). Should be " +
2730 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002731 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002732 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002734 auth_config = auth.extract_auth_config_from_options(options)
2735
2736 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002738 current = cl.GetBranch()
2739 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2740 if not settings.GetIsGitSvn() and remote == '.':
2741 print
2742 print 'Attempting to push branch %r into another local branch!' % current
2743 print
2744 print 'Either reparent this branch on top of origin/master:'
2745 print ' git reparent-branch --root'
2746 print
2747 print 'OR run `git rebase-update` if you think the parent branch is already'
2748 print 'committed.'
2749 print
2750 print ' Current parent: %r' % upstream_branch
2751 return 1
2752
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002753 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002754 # Default to merging against our best guess of the upstream branch.
2755 args = [cl.GetUpstreamBranch()]
2756
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002757 if options.contributor:
2758 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2759 print "Please provide contibutor as 'First Last <email@example.com>'"
2760 return 1
2761
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002762 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002763 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764
sbc@chromium.org71437c02015-04-09 19:29:40 +00002765 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002766 return 1
2767
2768 # This rev-list syntax means "show all commits not in my branch that
2769 # are in base_branch".
2770 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2771 base_branch]).splitlines()
2772 if upstream_commits:
2773 print ('Base branch "%s" has %d commits '
2774 'not in this branch.' % (base_branch, len(upstream_commits)))
2775 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2776 return 1
2777
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002778 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002779 svn_head = None
2780 if cmd == 'dcommit' or base_has_submodules:
2781 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2782 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002784 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002785 # If the base_head is a submodule merge commit, the first parent of the
2786 # base_head should be a git-svn commit, which is what we're interested in.
2787 base_svn_head = base_branch
2788 if base_has_submodules:
2789 base_svn_head += '^1'
2790
2791 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002792 if extra_commits:
2793 print ('This branch has %d additional commits not upstreamed yet.'
2794 % len(extra_commits.splitlines()))
2795 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2796 'before attempting to %s.' % (base_branch, cmd))
2797 return 1
2798
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002799 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002800 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002801 author = None
2802 if options.contributor:
2803 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002804 hook_results = cl.RunHook(
2805 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002806 may_prompt=not options.force,
2807 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002808 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002809 if not hook_results.should_continue():
2810 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002811
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002812 # Check the tree status if the tree status URL is set.
2813 status = GetTreeStatus()
2814 if 'closed' == status:
2815 print('The tree is closed. Please wait for it to reopen. Use '
2816 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2817 return 1
2818 elif 'unknown' == status:
2819 print('Unable to determine tree status. Please verify manually and '
2820 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2821 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002822
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002823 change_desc = ChangeDescription(options.message)
2824 if not change_desc.description and cl.GetIssue():
2825 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002826
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002828 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002829 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002830 else:
2831 print 'No description set.'
2832 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2833 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002834
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002835 # Keep a separate copy for the commit message, because the commit message
2836 # contains the link to the Rietveld issue, while the Rietveld message contains
2837 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002838 # Keep a separate copy for the commit message.
2839 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002840 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002841
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002842 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002843 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002844 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002845 # after it. Add a period on a new line to circumvent this. Also add a space
2846 # before the period to make sure that Gitiles continues to correctly resolve
2847 # the URL.
2848 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002850 commit_desc.append_footer('Patch from %s.' % options.contributor)
2851
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002852 print('Description:')
2853 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002854
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002855 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002856 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002857 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002859 # We want to squash all this branch's commits into one commit with the proper
2860 # description. We do this by doing a "reset --soft" to the base branch (which
2861 # keeps the working copy the same), then dcommitting that. If origin/master
2862 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2863 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002865 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2866 # Delete the branches if they exist.
2867 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2868 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2869 result = RunGitWithCode(showref_cmd)
2870 if result[0] == 0:
2871 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002872
2873 # We might be in a directory that's present in this branch but not in the
2874 # trunk. Move up to the top of the tree so that git commands that expect a
2875 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002876 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 if rel_base_path:
2878 os.chdir(rel_base_path)
2879
2880 # Stuff our change into the merge branch.
2881 # We wrap in a try...finally block so if anything goes wrong,
2882 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002883 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002884 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002885 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002886 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002887 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002888 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002889 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002890 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002891 RunGit(
2892 [
2893 'commit', '--author', options.contributor,
2894 '-m', commit_desc.description,
2895 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002896 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002897 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002898 if base_has_submodules:
2899 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2900 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2901 RunGit(['checkout', CHERRY_PICK_BRANCH])
2902 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002903 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002904 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002905 mirror = settings.GetGitMirror(remote)
2906 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002907 pending_prefix = settings.GetPendingRefPrefix()
2908 if not pending_prefix or branch.startswith(pending_prefix):
2909 # If not using refs/pending/heads/* at all, or target ref is already set
2910 # to pending, then push to the target ref directly.
2911 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002912 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002913 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002914 else:
2915 # Cherry-pick the change on top of pending ref and then push it.
2916 assert branch.startswith('refs/'), branch
2917 assert pending_prefix[-1] == '/', pending_prefix
2918 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00002919 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002920 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002921 if retcode == 0:
2922 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923 else:
2924 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002925 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002926 'svn', 'dcommit',
2927 '-C%s' % options.similarity,
2928 '--no-rebase', '--rmdir',
2929 ]
2930 if settings.GetForceHttpsCommitUrl():
2931 # Allow forcing https commit URLs for some projects that don't allow
2932 # committing to http URLs (like Google Code).
2933 remote_url = cl.GetGitSvnRemoteUrl()
2934 if urlparse.urlparse(remote_url).scheme == 'http':
2935 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002936 cmd_args.append('--commit-url=%s' % remote_url)
2937 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002938 if 'Committed r' in output:
2939 revision = re.match(
2940 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2941 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 finally:
2943 # And then swap back to the original branch and clean up.
2944 RunGit(['checkout', '-q', cl.GetBranch()])
2945 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002946 if base_has_submodules:
2947 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002949 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002950 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002951 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002952
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002953 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002954 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002955 try:
2956 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2957 # We set pushed_to_pending to False, since it made it all the way to the
2958 # real ref.
2959 pushed_to_pending = False
2960 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002961 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002963 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002964 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002966 if not to_pending:
2967 if viewvc_url and revision:
2968 change_desc.append_footer(
2969 'Committed: %s%s' % (viewvc_url, revision))
2970 elif revision:
2971 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 print ('Closing issue '
2973 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002974 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002975 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002976 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002977 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002978 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002979 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002980 if options.bypass_hooks:
2981 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2982 else:
2983 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002984 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002985 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002986
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002987 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002988 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2989 print 'The commit is in the pending queue (%s).' % pending_ref
2990 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002991 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002992 'footer.' % branch)
2993
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002994 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2995 if os.path.isfile(hook):
2996 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002997
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002998 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999
3000
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003001def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3002 print
3003 print 'Waiting for commit to be landed on %s...' % real_ref
3004 print '(If you are impatient, you may Ctrl-C once without harm)'
3005 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3006 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003007 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003008
3009 loop = 0
3010 while True:
3011 sys.stdout.write('fetching (%d)... \r' % loop)
3012 sys.stdout.flush()
3013 loop += 1
3014
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003015 if mirror:
3016 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003017 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3018 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3019 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3020 for commit in commits.splitlines():
3021 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3022 print 'Found commit on %s' % real_ref
3023 return commit
3024
3025 current_rev = to_rev
3026
3027
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003028def PushToGitPending(remote, pending_ref, upstream_ref):
3029 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3030
3031 Returns:
3032 (retcode of last operation, output log of last operation).
3033 """
3034 assert pending_ref.startswith('refs/'), pending_ref
3035 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3036 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3037 code = 0
3038 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003039 max_attempts = 3
3040 attempts_left = max_attempts
3041 while attempts_left:
3042 if attempts_left != max_attempts:
3043 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3044 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003045
3046 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003047 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003048 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003049 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003050 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003051 print 'Fetch failed with exit code %d.' % code
3052 if out.strip():
3053 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003054 continue
3055
3056 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003057 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003058 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003059 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003060 if code:
3061 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003062 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3063 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003064 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3065 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003066 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003067 return code, out
3068
3069 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003070 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003071 code, out = RunGitWithCode(
3072 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3073 if code == 0:
3074 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003075 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003076 return code, out
3077
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003078 print 'Push failed with exit code %d.' % code
3079 if out.strip():
3080 print out.strip()
3081 if IsFatalPushFailure(out):
3082 print (
3083 'Fatal push error. Make sure your .netrc credentials and git '
3084 'user.email are correct and you have push access to the repo.')
3085 return code, out
3086
3087 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003088 return code, out
3089
3090
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003091def IsFatalPushFailure(push_stdout):
3092 """True if retrying push won't help."""
3093 return '(prohibited by Gerrit)' in push_stdout
3094
3095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003096@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003098 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003100 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003101 # If it looks like previous commits were mirrored with git-svn.
3102 message = """This repository appears to be a git-svn mirror, but no
3103upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3104 else:
3105 message = """This doesn't appear to be an SVN repository.
3106If your project has a true, writeable git repository, you probably want to run
3107'git cl land' instead.
3108If your project has a git mirror of an upstream SVN master, you probably need
3109to run 'git svn init'.
3110
3111Using the wrong command might cause your commit to appear to succeed, and the
3112review to be closed, without actually landing upstream. If you choose to
3113proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003114 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003115 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116 return SendUpstream(parser, args, 'dcommit')
3117
3118
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003119@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003120def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003121 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003122 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123 print('This appears to be an SVN repository.')
3124 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003125 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003126 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003127 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128
3129
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003130def ParseIssueNum(arg):
3131 """Parses the issue number from args if present otherwise returns None."""
3132 if re.match(r'\d+', arg):
3133 return arg
3134 if arg.startswith('http'):
3135 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3136 return None
3137
3138
3139@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003140def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003141 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003142 parser.add_option('-b', dest='newbranch',
3143 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003144 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003146 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3147 help='Change to the directory DIR immediately, '
3148 'before doing anything else.')
3149 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003150 help='failed patches spew .rej files rather than '
3151 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003152 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3153 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003154
3155 group = optparse.OptionGroup(parser,
3156 """Options for continuing work on the current issue uploaded
3157from a different clone (e.g. different machine). Must be used independently from
3158the other options. No issue number should be specified, and the branch must have
3159an issue number associated with it""")
3160 group.add_option('--reapply', action='store_true',
3161 dest='reapply',
3162 help="""Reset the branch and reapply the issue.
3163CAUTION: This will undo any local changes in this branch""")
3164
3165 group.add_option('--pull', action='store_true', dest='pull',
3166 help="Performs a pull before reapplying.")
3167 parser.add_option_group(group)
3168
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003169 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003170 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003171 auth_config = auth.extract_auth_config_from_options(options)
3172
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003173 issue_arg = None
3174 if options.reapply :
3175 if len(args) > 0:
3176 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003177
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003178 cl = Changelist()
3179 issue_arg = cl.GetIssue()
3180 upstream = cl.GetUpstreamBranch()
3181 if upstream == None:
3182 parser.error("No upstream branch specified. Cannot reset branch")
3183
3184 RunGit(['reset', '--hard', upstream])
3185 if options.pull:
3186 RunGit(['pull'])
3187 else:
3188 if len(args) != 1:
3189 parser.error("Must specify issue number")
3190
3191 issue_arg = ParseIssueNum(args[0])
3192
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003193 # The patch URL works because ParseIssueNum won't do any substitution
3194 # as the re.sub pattern fails to match and just returns it.
3195 if issue_arg == None:
3196 parser.print_help()
3197 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003199 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003200 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003201 return 1
3202
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003203 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003204 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003205
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003206 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003207 if options.reapply:
3208 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003209 if options.force:
3210 RunGit(['branch', '-D', options.newbranch],
3211 stderr=subprocess2.PIPE, error_ok=True)
3212 RunGit(['checkout', '-b', options.newbranch,
3213 Changelist().GetUpstreamBranch()])
3214
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003215 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003216 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003217
3218
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003219def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003220 # PatchIssue should never be called with a dirty tree. It is up to the
3221 # caller to check this, but just in case we assert here since the
3222 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003223 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003224
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003225 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003227 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003228 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003229 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003230 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003232 # Assume it's a URL to the patch. Default to https.
3233 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003234 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003235 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236 DieWithError('Must pass an issue ID or full URL for '
3237 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003238 issue = int(match.group(2))
3239 cl = Changelist(issue=issue, auth_config=auth_config)
3240 cl.rietveld_server = match.group(1)
3241 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003242 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244 # Switch up to the top-level directory, if necessary, in preparation for
3245 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003246 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 if top:
3248 os.chdir(top)
3249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 # Git patches have a/ at the beginning of source paths. We strip that out
3251 # with a sed script rather than the -p flag to patch so we can feed either
3252 # Git or svn-style patches into the same apply command.
3253 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003254 try:
3255 patch_data = subprocess2.check_output(
3256 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3257 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003258 DieWithError('Git patch mungling failed.')
3259 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261 # We use "git apply" to apply the patch instead of "patch" so that we can
3262 # pick up file adds.
3263 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003264 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003265 if directory:
3266 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003267 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003268 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003269 elif IsGitVersionAtLeast('1.7.12'):
3270 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003271 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003272 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003273 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003274 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003275 print 'Failed to apply the patch'
3276 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277
3278 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003279 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003280 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3281 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003282 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3283 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003284 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003286 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003287 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003288 else:
3289 print "Patch applied to index."
3290 return 0
3291
3292
3293def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003294 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003295 # Provide a wrapper for git svn rebase to help avoid accidental
3296 # git svn dcommit.
3297 # It's the only command that doesn't use parser at all since we just defer
3298 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003299
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003300 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301
3302
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003303def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 """Fetches the tree status and returns either 'open', 'closed',
3305 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003306 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003307 if url:
3308 status = urllib2.urlopen(url).read().lower()
3309 if status.find('closed') != -1 or status == '0':
3310 return 'closed'
3311 elif status.find('open') != -1 or status == '1':
3312 return 'open'
3313 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003314 return 'unset'
3315
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317def GetTreeStatusReason():
3318 """Fetches the tree status from a json url and returns the message
3319 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003320 url = settings.GetTreeStatusUrl()
3321 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322 connection = urllib2.urlopen(json_url)
3323 status = json.loads(connection.read())
3324 connection.close()
3325 return status['message']
3326
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003327
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003328def GetBuilderMaster(bot_list):
3329 """For a given builder, fetch the master from AE if available."""
3330 map_url = 'https://builders-map.appspot.com/'
3331 try:
3332 master_map = json.load(urllib2.urlopen(map_url))
3333 except urllib2.URLError as e:
3334 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3335 (map_url, e))
3336 except ValueError as e:
3337 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3338 if not master_map:
3339 return None, 'Failed to build master map.'
3340
3341 result_master = ''
3342 for bot in bot_list:
3343 builder = bot.split(':', 1)[0]
3344 master_list = master_map.get(builder, [])
3345 if not master_list:
3346 return None, ('No matching master for builder %s.' % builder)
3347 elif len(master_list) > 1:
3348 return None, ('The builder name %s exists in multiple masters %s.' %
3349 (builder, master_list))
3350 else:
3351 cur_master = master_list[0]
3352 if not result_master:
3353 result_master = cur_master
3354 elif result_master != cur_master:
3355 return None, 'The builders do not belong to the same master.'
3356 return result_master, None
3357
3358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003359def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003360 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003361 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 status = GetTreeStatus()
3363 if 'unset' == status:
3364 print 'You must configure your tree status URL by running "git cl config".'
3365 return 2
3366
3367 print "The tree is %s" % status
3368 print
3369 print GetTreeStatusReason()
3370 if status != 'open':
3371 return 1
3372 return 0
3373
3374
maruel@chromium.org15192402012-09-06 12:38:29 +00003375def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003376 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003377 group = optparse.OptionGroup(parser, "Try job options")
3378 group.add_option(
3379 "-b", "--bot", action="append",
3380 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3381 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003382 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003383 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003384 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003385 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003386 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003387 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003388 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003389 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003390 "-r", "--revision",
3391 help="Revision to use for the try job; default: the "
3392 "revision will be determined by the try server; see "
3393 "its waterfall for more info")
3394 group.add_option(
3395 "-c", "--clobber", action="store_true", default=False,
3396 help="Force a clobber before building; e.g. don't do an "
3397 "incremental build")
3398 group.add_option(
3399 "--project",
3400 help="Override which project to use. Projects are defined "
3401 "server-side to define what default bot set to use")
3402 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003403 "-p", "--property", dest="properties", action="append", default=[],
3404 help="Specify generic properties in the form -p key1=value1 -p "
3405 "key2=value2 etc (buildbucket only). The value will be treated as "
3406 "json if decodable, or as string otherwise.")
3407 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003408 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003409 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003410 "--use-rietveld", action="store_true", default=False,
3411 help="Use Rietveld to trigger try jobs.")
3412 group.add_option(
3413 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3414 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003415 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003416 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003417 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003418 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003419
machenbach@chromium.org45453142015-09-15 08:45:22 +00003420 if options.use_rietveld and options.properties:
3421 parser.error('Properties can only be specified with buildbucket')
3422
3423 # Make sure that all properties are prop=value pairs.
3424 bad_params = [x for x in options.properties if '=' not in x]
3425 if bad_params:
3426 parser.error('Got properties with missing "=": %s' % bad_params)
3427
maruel@chromium.org15192402012-09-06 12:38:29 +00003428 if args:
3429 parser.error('Unknown arguments: %s' % args)
3430
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003431 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003432 if not cl.GetIssue():
3433 parser.error('Need to upload first')
3434
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003435 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003436 if props.get('closed'):
3437 parser.error('Cannot send tryjobs for a closed CL')
3438
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003439 if props.get('private'):
3440 parser.error('Cannot use trybots with private issue')
3441
maruel@chromium.org15192402012-09-06 12:38:29 +00003442 if not options.name:
3443 options.name = cl.GetBranch()
3444
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003445 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003446 options.master, err_msg = GetBuilderMaster(options.bot)
3447 if err_msg:
3448 parser.error('Tryserver master cannot be found because: %s\n'
3449 'Please manually specify the tryserver master'
3450 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003451
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003452 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003453 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003454 if not options.bot:
3455 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003456
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003457 # Get try masters from PRESUBMIT.py files.
3458 masters = presubmit_support.DoGetTryMasters(
3459 change,
3460 change.LocalPaths(),
3461 settings.GetRoot(),
3462 None,
3463 None,
3464 options.verbose,
3465 sys.stdout)
3466 if masters:
3467 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003468
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003469 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3470 options.bot = presubmit_support.DoGetTrySlaves(
3471 change,
3472 change.LocalPaths(),
3473 settings.GetRoot(),
3474 None,
3475 None,
3476 options.verbose,
3477 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003478
3479 if not options.bot:
3480 # Get try masters from cq.cfg if any.
3481 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3482 # location.
3483 cq_cfg = os.path.join(change.RepositoryRoot(),
3484 'infra', 'config', 'cq.cfg')
3485 if os.path.exists(cq_cfg):
3486 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003487 cq_masters = commit_queue.get_master_builder_map(
3488 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003489 for master, builders in cq_masters.iteritems():
3490 for builder in builders:
3491 # Skip presubmit builders, because these will fail without LGTM.
3492 if 'presubmit' not in builder.lower():
3493 masters.setdefault(master, {})[builder] = ['defaulttests']
3494 if masters:
3495 return masters
3496
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003497 if not options.bot:
3498 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003499
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003500 builders_and_tests = {}
3501 # TODO(machenbach): The old style command-line options don't support
3502 # multiple try masters yet.
3503 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3504 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3505
3506 for bot in old_style:
3507 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003508 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003509 elif ',' in bot:
3510 parser.error('Specify one bot per --bot flag')
3511 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003512 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003513
3514 for bot, tests in new_style:
3515 builders_and_tests.setdefault(bot, []).extend(tests)
3516
3517 # Return a master map with one master to be backwards compatible. The
3518 # master name defaults to an empty string, which will cause the master
3519 # not to be set on rietveld (deprecated).
3520 return {options.master: builders_and_tests}
3521
3522 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003523
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003524 for builders in masters.itervalues():
3525 if any('triggered' in b for b in builders):
3526 print >> sys.stderr, (
3527 'ERROR You are trying to send a job to a triggered bot. This type of'
3528 ' bot requires an\ninitial job from a parent (usually a builder). '
3529 'Instead send your job to the parent.\n'
3530 'Bot list: %s' % builders)
3531 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003532
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003533 patchset = cl.GetMostRecentPatchset()
3534 if patchset and patchset != cl.GetPatchset():
3535 print(
3536 '\nWARNING Mismatch between local config and server. Did a previous '
3537 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3538 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003539 if options.luci:
3540 trigger_luci_job(cl, masters, options)
3541 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003542 try:
3543 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3544 except BuildbucketResponseException as ex:
3545 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003546 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003547 except Exception as e:
3548 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3549 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3550 e, stacktrace)
3551 return 1
3552 else:
3553 try:
3554 cl.RpcServer().trigger_distributed_try_jobs(
3555 cl.GetIssue(), patchset, options.name, options.clobber,
3556 options.revision, masters)
3557 except urllib2.HTTPError as e:
3558 if e.code == 404:
3559 print('404 from rietveld; '
3560 'did you mean to use "git try" instead of "git cl try"?')
3561 return 1
3562 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003563
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003564 for (master, builders) in sorted(masters.iteritems()):
3565 if master:
3566 print 'Master: %s' % master
3567 length = max(len(builder) for builder in builders)
3568 for builder in sorted(builders):
3569 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003570 return 0
3571
3572
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003573def CMDtry_results(parser, args):
3574 group = optparse.OptionGroup(parser, "Try job results options")
3575 group.add_option(
3576 "-p", "--patchset", type=int, help="patchset number if not current.")
3577 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003578 "--print-master", action='store_true', help="print master name as well.")
3579 group.add_option(
3580 "--color", action='store_true', default=sys.stdout.isatty(),
3581 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003582 group.add_option(
3583 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3584 help="Host of buildbucket. The default host is %default.")
3585 parser.add_option_group(group)
3586 auth.add_auth_options(parser)
3587 options, args = parser.parse_args(args)
3588 if args:
3589 parser.error('Unrecognized args: %s' % ' '.join(args))
3590
3591 auth_config = auth.extract_auth_config_from_options(options)
3592 cl = Changelist(auth_config=auth_config)
3593 if not cl.GetIssue():
3594 parser.error('Need to upload first')
3595
3596 if not options.patchset:
3597 options.patchset = cl.GetMostRecentPatchset()
3598 if options.patchset and options.patchset != cl.GetPatchset():
3599 print(
3600 '\nWARNING Mismatch between local config and server. Did a previous '
3601 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3602 'Continuing using\npatchset %s.\n' % options.patchset)
3603 try:
3604 jobs = fetch_try_jobs(auth_config, cl, options)
3605 except BuildbucketResponseException as ex:
3606 print 'Buildbucket error: %s' % ex
3607 return 1
3608 except Exception as e:
3609 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3610 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3611 e, stacktrace)
3612 return 1
3613 print_tryjobs(options, jobs)
3614 return 0
3615
3616
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003617@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003619 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003620 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003621 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003622 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003625 if args:
3626 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003627 branch = cl.GetBranch()
3628 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003629 cl = Changelist()
3630 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003631
3632 # Clear configured merge-base, if there is one.
3633 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003634 else:
3635 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 return 0
3637
3638
thestig@chromium.org00858c82013-12-02 23:08:03 +00003639def CMDweb(parser, args):
3640 """Opens the current CL in the web browser."""
3641 _, args = parser.parse_args(args)
3642 if args:
3643 parser.error('Unrecognized args: %s' % ' '.join(args))
3644
3645 issue_url = Changelist().GetIssueURL()
3646 if not issue_url:
3647 print >> sys.stderr, 'ERROR No issue to open'
3648 return 1
3649
3650 webbrowser.open(issue_url)
3651 return 0
3652
3653
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003654def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003655 """Sets the commit bit to trigger the Commit Queue."""
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)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003659 if args:
3660 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003661 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003662 props = cl.GetIssueProperties()
3663 if props.get('private'):
3664 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003665 cl.SetFlag('commit', '1')
3666 return 0
3667
3668
groby@chromium.org411034a2013-02-26 15:12:01 +00003669def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003670 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003671 auth.add_auth_options(parser)
3672 options, args = parser.parse_args(args)
3673 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003674 if args:
3675 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003676 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003677 # Ensure there actually is an issue to close.
3678 cl.GetDescription()
3679 cl.CloseIssue()
3680 return 0
3681
3682
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003683def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003684 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003685 auth.add_auth_options(parser)
3686 options, args = parser.parse_args(args)
3687 auth_config = auth.extract_auth_config_from_options(options)
3688 if args:
3689 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003690
3691 # Uncommitted (staged and unstaged) changes will be destroyed by
3692 # "git reset --hard" if there are merging conflicts in PatchIssue().
3693 # Staged changes would be committed along with the patch from last
3694 # upload, hence counted toward the "last upload" side in the final
3695 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003696 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003697 return 1
3698
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003699 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003700 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003701 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003702 if not issue:
3703 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003704 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003705 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003706
3707 # Create a new branch based on the merge-base
3708 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3709 try:
3710 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003711 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003712 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003713 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003714 return rtn
3715
wychen@chromium.org06928532015-02-03 02:11:29 +00003716 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003717 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003718 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003719 finally:
3720 RunGit(['checkout', '-q', branch])
3721 RunGit(['branch', '-D', TMP_BRANCH])
3722
3723 return 0
3724
3725
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003726def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003727 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003728 parser.add_option(
3729 '--no-color',
3730 action='store_true',
3731 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003732 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003733 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003734 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003735
3736 author = RunGit(['config', 'user.email']).strip() or None
3737
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003738 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003739
3740 if args:
3741 if len(args) > 1:
3742 parser.error('Unknown args')
3743 base_branch = args[0]
3744 else:
3745 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003746 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003747
3748 change = cl.GetChange(base_branch, None)
3749 return owners_finder.OwnersFinder(
3750 [f.LocalPath() for f in
3751 cl.GetChange(base_branch, None).AffectedFiles()],
3752 change.RepositoryRoot(), author,
3753 fopen=file, os_path=os.path, glob=glob.glob,
3754 disable_color=options.no_color).run()
3755
3756
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003757def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003758 """Generates a diff command."""
3759 # Generate diff for the current branch's changes.
3760 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3761 upstream_commit, '--' ]
3762
3763 if args:
3764 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003765 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003766 diff_cmd.append(arg)
3767 else:
3768 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003769
3770 return diff_cmd
3771
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003772def MatchingFileType(file_name, extensions):
3773 """Returns true if the file name ends with one of the given extensions."""
3774 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003775
enne@chromium.org555cfe42014-01-29 18:21:39 +00003776@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003777def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003778 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003779 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003780 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003781 parser.add_option('--full', action='store_true',
3782 help='Reformat the full content of all touched files')
3783 parser.add_option('--dry-run', action='store_true',
3784 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003785 parser.add_option('--python', action='store_true',
3786 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003787 parser.add_option('--diff', action='store_true',
3788 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003789 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003790
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003791 # git diff generates paths against the root of the repository. Change
3792 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003793 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003794 if rel_base_path:
3795 os.chdir(rel_base_path)
3796
digit@chromium.org29e47272013-05-17 17:01:46 +00003797 # Grab the merge-base commit, i.e. the upstream commit of the current
3798 # branch when it was created or the last time it was rebased. This is
3799 # to cover the case where the user may have called "git fetch origin",
3800 # moving the origin branch to a newer commit, but hasn't rebased yet.
3801 upstream_commit = None
3802 cl = Changelist()
3803 upstream_branch = cl.GetUpstreamBranch()
3804 if upstream_branch:
3805 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3806 upstream_commit = upstream_commit.strip()
3807
3808 if not upstream_commit:
3809 DieWithError('Could not find base commit for this branch. '
3810 'Are you in detached state?')
3811
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003812 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3813 diff_output = RunGit(changed_files_cmd)
3814 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003815 # Filter out files deleted by this CL
3816 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003817
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003818 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3819 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3820 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003821 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003822
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003823 top_dir = os.path.normpath(
3824 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3825
3826 # Locate the clang-format binary in the checkout
3827 try:
3828 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3829 except clang_format.NotFoundError, e:
3830 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003831
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003832 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3833 # formatted. This is used to block during the presubmit.
3834 return_value = 0
3835
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003836 if clang_diff_files:
3837 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003838 cmd = [clang_format_tool]
3839 if not opts.dry_run and not opts.diff:
3840 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003841 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003842 if opts.diff:
3843 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003844 else:
3845 env = os.environ.copy()
3846 env['PATH'] = str(os.path.dirname(clang_format_tool))
3847 try:
3848 script = clang_format.FindClangFormatScriptInChromiumTree(
3849 'clang-format-diff.py')
3850 except clang_format.NotFoundError, e:
3851 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003852
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003853 cmd = [sys.executable, script, '-p0']
3854 if not opts.dry_run and not opts.diff:
3855 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003856
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003857 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3858 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003859
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003860 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3861 if opts.diff:
3862 sys.stdout.write(stdout)
3863 if opts.dry_run and len(stdout) > 0:
3864 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003865
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003866 # Similar code to above, but using yapf on .py files rather than clang-format
3867 # on C/C++ files
3868 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003869 yapf_tool = gclient_utils.FindExecutable('yapf')
3870 if yapf_tool is None:
3871 DieWithError('yapf not found in PATH')
3872
3873 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003874 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003875 cmd = [yapf_tool]
3876 if not opts.dry_run and not opts.diff:
3877 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003878 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003879 if opts.diff:
3880 sys.stdout.write(stdout)
3881 else:
3882 # TODO(sbc): yapf --lines mode still has some issues.
3883 # https://github.com/google/yapf/issues/154
3884 DieWithError('--python currently only works with --full')
3885
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003886 # Dart's formatter does not have the nice property of only operating on
3887 # modified chunks, so hard code full.
3888 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003889 try:
3890 command = [dart_format.FindDartFmtToolInChromiumTree()]
3891 if not opts.dry_run and not opts.diff:
3892 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003893 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003894
ppi@chromium.org6593d932016-03-03 15:41:15 +00003895 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003896 if opts.dry_run and stdout:
3897 return_value = 2
3898 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003899 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3900 'found in this checkout. Files in other languages are still ' +
3901 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003902
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003903 # Format GN build files. Always run on full build files for canonical form.
3904 if gn_diff_files:
3905 cmd = ['gn', 'format']
3906 if not opts.dry_run and not opts.diff:
3907 cmd.append('--in-place')
3908 for gn_diff_file in gn_diff_files:
3909 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3910 if opts.diff:
3911 sys.stdout.write(stdout)
3912
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003913 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003914
3915
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003916@subcommand.usage('<codereview url or issue id>')
3917def CMDcheckout(parser, args):
3918 """Checks out a branch associated with a given Rietveld issue."""
3919 _, args = parser.parse_args(args)
3920
3921 if len(args) != 1:
3922 parser.print_help()
3923 return 1
3924
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003925 target_issue = ParseIssueNum(args[0])
3926 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003927 parser.print_help()
3928 return 1
3929
3930 key_and_issues = [x.split() for x in RunGit(
3931 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3932 .splitlines()]
3933 branches = []
3934 for key, issue in key_and_issues:
3935 if issue == target_issue:
3936 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3937
3938 if len(branches) == 0:
3939 print 'No branch found for issue %s.' % target_issue
3940 return 1
3941 if len(branches) == 1:
3942 RunGit(['checkout', branches[0]])
3943 else:
3944 print 'Multiple branches match issue %s:' % target_issue
3945 for i in range(len(branches)):
3946 print '%d: %s' % (i, branches[i])
3947 which = raw_input('Choose by index: ')
3948 try:
3949 RunGit(['checkout', branches[int(which)]])
3950 except (IndexError, ValueError):
3951 print 'Invalid selection, not checking out any branch.'
3952 return 1
3953
3954 return 0
3955
3956
maruel@chromium.org29404b52014-09-08 22:58:00 +00003957def CMDlol(parser, args):
3958 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003959 print zlib.decompress(base64.b64decode(
3960 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3961 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3962 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3963 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003964 return 0
3965
3966
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003967class OptionParser(optparse.OptionParser):
3968 """Creates the option parse and add --verbose support."""
3969 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003970 optparse.OptionParser.__init__(
3971 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003972 self.add_option(
3973 '-v', '--verbose', action='count', default=0,
3974 help='Use 2 times for more debugging info')
3975
3976 def parse_args(self, args=None, values=None):
3977 options, args = optparse.OptionParser.parse_args(self, args, values)
3978 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3979 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3980 return options, args
3981
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003982
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003983def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003984 if sys.hexversion < 0x02060000:
3985 print >> sys.stderr, (
3986 '\nYour python version %s is unsupported, please upgrade.\n' %
3987 sys.version.split(' ', 1)[0])
3988 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003989
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003990 # Reload settings.
3991 global settings
3992 settings = Settings()
3993
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003994 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003995 dispatcher = subcommand.CommandDispatcher(__name__)
3996 try:
3997 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003998 except auth.AuthenticationError as e:
3999 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004000 except urllib2.HTTPError, e:
4001 if e.code != 500:
4002 raise
4003 DieWithError(
4004 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4005 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004006 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007
4008
4009if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004010 # These affect sys.stdout so do it outside of main() to simplify mocks in
4011 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004012 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004013 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004014 try:
4015 sys.exit(main(sys.argv[1:]))
4016 except KeyboardInterrupt:
4017 sys.stderr.write('interrupted\n')
4018 sys.exit(1)