blob: d236e8596a8cf832819d6ae39f65d0904b116d4b [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
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000051import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000052import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000055import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000063DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000064POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000066GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000067REFS_THAT_ALIAS_TO_OTHER_REFS = {
68 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
69 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
70}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000142def BranchExists(branch):
143 """Return True if specified branch exists."""
144 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
145 suppress_stderr=True)
146 return not code
147
148
maruel@chromium.org90541732011-04-01 17:54:18 +0000149def ask_for_data(prompt):
150 try:
151 return raw_input(prompt)
152 except KeyboardInterrupt:
153 # Hide the exception.
154 sys.exit(1)
155
156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157def git_set_branch_value(key, value):
158 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000159 if not branch:
160 return
161
162 cmd = ['config']
163 if isinstance(value, int):
164 cmd.append('--int')
165 git_key = 'branch.%s.%s' % (branch, key)
166 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167
168
169def git_get_branch_default(key, default):
170 branch = Changelist().GetBranch()
171 if branch:
172 git_key = 'branch.%s.%s' % (branch, key)
173 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
174 try:
175 return int(stdout.strip())
176 except ValueError:
177 pass
178 return default
179
180
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181def add_git_similarity(parser):
182 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 help='Sets the percentage that a pair of files need to match in order to'
185 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 parser.add_option(
187 '--find-copies', action='store_true',
188 help='Allows git to look for copies.')
189 parser.add_option(
190 '--no-find-copies', action='store_false', dest='find_copies',
191 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 old_parser_args = parser.parse_args
194 def Parse(args):
195 options, args = old_parser_args(args)
196
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 print('Note: Saving similarity of %d%% in git config.'
201 % options.similarity)
202 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 options.similarity = max(0, min(options.similarity, 100))
205
206 if options.find_copies is None:
207 options.find_copies = bool(
208 git_get_branch_default('git-find-copies', True))
209 else:
210 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000211
212 print('Using %d%% similarity for rename/copy detection. '
213 'Override with --similarity.' % options.similarity)
214
215 return options, args
216 parser.parse_args = Parse
217
218
machenbach@chromium.org45453142015-09-15 08:45:22 +0000219def _get_properties_from_options(options):
220 properties = dict(x.split('=', 1) for x in options.properties)
221 for key, val in properties.iteritems():
222 try:
223 properties[key] = json.loads(val)
224 except ValueError:
225 pass # If a value couldn't be evaluated, treat it as a string.
226 return properties
227
228
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000229def _prefix_master(master):
230 """Convert user-specified master name to full master name.
231
232 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
233 name, while the developers always use shortened master name
234 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
235 function does the conversion for buildbucket migration.
236 """
237 prefix = 'master.'
238 if master.startswith(prefix):
239 return master
240 return '%s%s' % (prefix, master)
241
242
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000243def _buildbucket_retry(operation_name, http, *args, **kwargs):
244 """Retries requests to buildbucket service and returns parsed json content."""
245 try_count = 0
246 while True:
247 response, content = http.request(*args, **kwargs)
248 try:
249 content_json = json.loads(content)
250 except ValueError:
251 content_json = None
252
253 # Buildbucket could return an error even if status==200.
254 if content_json and content_json.get('error'):
255 msg = 'Error in response. Reason: %s. Message: %s.' % (
256 content_json['error'].get('reason', ''),
257 content_json['error'].get('message', ''))
258 raise BuildbucketResponseException(msg)
259
260 if response.status == 200:
261 if not content_json:
262 raise BuildbucketResponseException(
263 'Buildbucket returns invalid json content: %s.\n'
264 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
265 content)
266 return content_json
267 if response.status < 500 or try_count >= 2:
268 raise httplib2.HttpLib2Error(content)
269
270 # status >= 500 means transient failures.
271 logging.debug('Transient errors when %s. Will retry.', operation_name)
272 time.sleep(0.5 + 1.5*try_count)
273 try_count += 1
274 assert False, 'unreachable'
275
276
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000277def trigger_luci_job(changelist, masters, options):
278 """Send a job to run on LUCI."""
279 issue_props = changelist.GetIssueProperties()
280 issue = changelist.GetIssue()
281 patchset = changelist.GetMostRecentPatchset()
282 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000283 # TODO(hinoka et al): add support for other properties.
284 # Currently, this completely ignores testfilter and other properties.
285 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000286 luci_trigger.trigger(
287 builder, 'HEAD', issue, patchset, issue_props['project'])
288
289
machenbach@chromium.org45453142015-09-15 08:45:22 +0000290def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000291 rietveld_url = settings.GetDefaultServerUrl()
292 rietveld_host = urlparse.urlparse(rietveld_url).hostname
293 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
294 http = authenticator.authorize(httplib2.Http())
295 http.force_exception_to_status_code = True
296 issue_props = changelist.GetIssueProperties()
297 issue = changelist.GetIssue()
298 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000299 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000300
301 buildbucket_put_url = (
302 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000303 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
305 hostname=rietveld_host,
306 issue=issue,
307 patch=patchset)
308
309 batch_req_body = {'builds': []}
310 print_text = []
311 print_text.append('Tried jobs on:')
312 for master, builders_and_tests in sorted(masters.iteritems()):
313 print_text.append('Master: %s' % master)
314 bucket = _prefix_master(master)
315 for builder, tests in sorted(builders_and_tests.iteritems()):
316 print_text.append(' %s: %s' % (builder, tests))
317 parameters = {
318 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000319 'changes': [{
320 'author': {'email': issue_props['owner_email']},
321 'revision': options.revision,
322 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 'properties': {
324 'category': category,
325 'issue': issue,
326 'master': master,
327 'patch_project': issue_props['project'],
328 'patch_storage': 'rietveld',
329 'patchset': patchset,
330 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332 },
333 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000334 if tests:
335 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000336 if properties:
337 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 if options.clobber:
339 parameters['properties']['clobber'] = True
340 batch_req_body['builds'].append(
341 {
342 'bucket': bucket,
343 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345 'tags': ['builder:%s' % builder,
346 'buildset:%s' % buildset,
347 'master:%s' % master,
348 'user_agent:git_cl_try']
349 }
350 )
351
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 _buildbucket_retry(
353 'triggering tryjobs',
354 http,
355 buildbucket_put_url,
356 'PUT',
357 body=json.dumps(batch_req_body),
358 headers={'Content-Type': 'application/json'}
359 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000360 print_text.append('To see results here, run: git cl try-results')
361 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000363
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000364
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000365def fetch_try_jobs(auth_config, changelist, options):
366 """Fetches tryjobs from buildbucket.
367
368 Returns a map from build id to build info as json dictionary.
369 """
370 rietveld_url = settings.GetDefaultServerUrl()
371 rietveld_host = urlparse.urlparse(rietveld_url).hostname
372 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
373 if authenticator.has_cached_credentials():
374 http = authenticator.authorize(httplib2.Http())
375 else:
376 print ('Warning: Some results might be missing because %s' %
377 # Get the message on how to login.
378 auth.LoginRequiredError(rietveld_host).message)
379 http = httplib2.Http()
380
381 http.force_exception_to_status_code = True
382
383 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
384 hostname=rietveld_host,
385 issue=changelist.GetIssue(),
386 patch=options.patchset)
387 params = {'tag': 'buildset:%s' % buildset}
388
389 builds = {}
390 while True:
391 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
392 hostname=options.buildbucket_host,
393 params=urllib.urlencode(params))
394 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
395 for build in content.get('builds', []):
396 builds[build['id']] = build
397 if 'next_cursor' in content:
398 params['start_cursor'] = content['next_cursor']
399 else:
400 break
401 return builds
402
403
404def print_tryjobs(options, builds):
405 """Prints nicely result of fetch_try_jobs."""
406 if not builds:
407 print 'No tryjobs scheduled'
408 return
409
410 # Make a copy, because we'll be modifying builds dictionary.
411 builds = builds.copy()
412 builder_names_cache = {}
413
414 def get_builder(b):
415 try:
416 return builder_names_cache[b['id']]
417 except KeyError:
418 try:
419 parameters = json.loads(b['parameters_json'])
420 name = parameters['builder_name']
421 except (ValueError, KeyError) as error:
422 print 'WARNING: failed to get builder name for build %s: %s' % (
423 b['id'], error)
424 name = None
425 builder_names_cache[b['id']] = name
426 return name
427
428 def get_bucket(b):
429 bucket = b['bucket']
430 if bucket.startswith('master.'):
431 return bucket[len('master.'):]
432 return bucket
433
434 if options.print_master:
435 name_fmt = '%%-%ds %%-%ds' % (
436 max(len(str(get_bucket(b))) for b in builds.itervalues()),
437 max(len(str(get_builder(b))) for b in builds.itervalues()))
438 def get_name(b):
439 return name_fmt % (get_bucket(b), get_builder(b))
440 else:
441 name_fmt = '%%-%ds' % (
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % get_builder(b)
445
446 def sort_key(b):
447 return b['status'], b.get('result'), get_name(b), b.get('url')
448
449 def pop(title, f, color=None, **kwargs):
450 """Pop matching builds from `builds` dict and print them."""
451
452 if not sys.stdout.isatty() or color is None:
453 colorize = str
454 else:
455 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
456
457 result = []
458 for b in builds.values():
459 if all(b.get(k) == v for k, v in kwargs.iteritems()):
460 builds.pop(b['id'])
461 result.append(b)
462 if result:
463 print colorize(title)
464 for b in sorted(result, key=sort_key):
465 print ' ', colorize('\t'.join(map(str, f(b))))
466
467 total = len(builds)
468 pop(status='COMPLETED', result='SUCCESS',
469 title='Successes:', color=Fore.GREEN,
470 f=lambda b: (get_name(b), b.get('url')))
471 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
472 title='Infra Failures:', color=Fore.MAGENTA,
473 f=lambda b: (get_name(b), b.get('url')))
474 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
475 title='Failures:', color=Fore.RED,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='CANCELED',
478 title='Canceled:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b),))
480 pop(status='COMPLETED', result='FAILURE',
481 failure_reason='INVALID_BUILD_DEFINITION',
482 title='Wrong master/builder name:', color=Fore.MAGENTA,
483 f=lambda b: (get_name(b),))
484 pop(status='COMPLETED', result='FAILURE',
485 title='Other failures:',
486 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
487 pop(status='COMPLETED',
488 title='Other finished:',
489 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
490 pop(status='STARTED',
491 title='Started:', color=Fore.YELLOW,
492 f=lambda b: (get_name(b), b.get('url')))
493 pop(status='SCHEDULED',
494 title='Scheduled:',
495 f=lambda b: (get_name(b), 'id=%s' % b['id']))
496 # The last section is just in case buildbucket API changes OR there is a bug.
497 pop(title='Other:',
498 f=lambda b: (get_name(b), 'id=%s' % b['id']))
499 assert len(builds) == 0
500 print 'Total: %d tryjobs' % total
501
502
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000503def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
504 """Return the corresponding git ref if |base_url| together with |glob_spec|
505 matches the full |url|.
506
507 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
508 """
509 fetch_suburl, as_ref = glob_spec.split(':')
510 if allow_wildcards:
511 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
512 if glob_match:
513 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
514 # "branches/{472,597,648}/src:refs/remotes/svn/*".
515 branch_re = re.escape(base_url)
516 if glob_match.group(1):
517 branch_re += '/' + re.escape(glob_match.group(1))
518 wildcard = glob_match.group(2)
519 if wildcard == '*':
520 branch_re += '([^/]*)'
521 else:
522 # Escape and replace surrounding braces with parentheses and commas
523 # with pipe symbols.
524 wildcard = re.escape(wildcard)
525 wildcard = re.sub('^\\\\{', '(', wildcard)
526 wildcard = re.sub('\\\\,', '|', wildcard)
527 wildcard = re.sub('\\\\}$', ')', wildcard)
528 branch_re += wildcard
529 if glob_match.group(3):
530 branch_re += re.escape(glob_match.group(3))
531 match = re.match(branch_re, url)
532 if match:
533 return re.sub('\*$', match.group(1), as_ref)
534
535 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
536 if fetch_suburl:
537 full_url = base_url + '/' + fetch_suburl
538 else:
539 full_url = base_url
540 if full_url == url:
541 return as_ref
542 return None
543
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000544
iannucci@chromium.org79540052012-10-19 23:15:26 +0000545def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000546 """Prints statistics about the change to the user."""
547 # --no-ext-diff is broken in some versions of Git, so try to work around
548 # this by overriding the environment (but there is still a problem if the
549 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000550 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 if 'GIT_EXTERNAL_DIFF' in env:
552 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000553
554 if find_copies:
555 similarity_options = ['--find-copies-harder', '-l100000',
556 '-C%s' % similarity]
557 else:
558 similarity_options = ['-M%s' % similarity]
559
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000560 try:
561 stdout = sys.stdout.fileno()
562 except AttributeError:
563 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000564 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000565 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000566 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000567 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000568
569
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000570class BuildbucketResponseException(Exception):
571 pass
572
573
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574class Settings(object):
575 def __init__(self):
576 self.default_server = None
577 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000578 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579 self.is_git_svn = None
580 self.svn_branch = None
581 self.tree_status_url = None
582 self.viewvc_url = None
583 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000584 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000585 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000586 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000587 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000588 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000589 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590
591 def LazyUpdateIfNeeded(self):
592 """Updates the settings from a codereview.settings file, if available."""
593 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000594 # The only value that actually changes the behavior is
595 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000596 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000597 error_ok=True
598 ).strip().lower()
599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000601 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 LoadCodereviewSettingsFromFile(cr_settings_file)
603 self.updated = True
604
605 def GetDefaultServerUrl(self, error_ok=False):
606 if not self.default_server:
607 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000608 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000609 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 if error_ok:
611 return self.default_server
612 if not self.default_server:
613 error_message = ('Could not find settings file. You must configure '
614 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000615 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000616 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 return self.default_server
618
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000619 @staticmethod
620 def GetRelativeRoot():
621 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 if self.root is None:
625 self.root = os.path.abspath(self.GetRelativeRoot())
626 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627
628 def GetIsGitSvn(self):
629 """Return true if this repo looks like it's using git-svn."""
630 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000631 if self.GetPendingRefPrefix():
632 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
633 self.is_git_svn = False
634 else:
635 # If you have any "svn-remote.*" config keys, we think you're using svn.
636 self.is_git_svn = RunGitWithCode(
637 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 return self.is_git_svn
639
640 def GetSVNBranch(self):
641 if self.svn_branch is None:
642 if not self.GetIsGitSvn():
643 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
644
645 # Try to figure out which remote branch we're based on.
646 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000647 # 1) iterate through our branch history and find the svn URL.
648 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649
650 # regexp matching the git-svn line that contains the URL.
651 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
652
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000653 # We don't want to go through all of history, so read a line from the
654 # pipe at a time.
655 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000656 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000657 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
658 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000659 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000660 for line in proc.stdout:
661 match = git_svn_re.match(line)
662 if match:
663 url = match.group(1)
664 proc.stdout.close() # Cut pipe.
665 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000667 if url:
668 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
669 remotes = RunGit(['config', '--get-regexp',
670 r'^svn-remote\..*\.url']).splitlines()
671 for remote in remotes:
672 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000674 remote = match.group(1)
675 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000676 rewrite_root = RunGit(
677 ['config', 'svn-remote.%s.rewriteRoot' % remote],
678 error_ok=True).strip()
679 if rewrite_root:
680 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000681 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000682 ['config', 'svn-remote.%s.fetch' % remote],
683 error_ok=True).strip()
684 if fetch_spec:
685 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
686 if self.svn_branch:
687 break
688 branch_spec = RunGit(
689 ['config', 'svn-remote.%s.branches' % remote],
690 error_ok=True).strip()
691 if branch_spec:
692 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
693 if self.svn_branch:
694 break
695 tag_spec = RunGit(
696 ['config', 'svn-remote.%s.tags' % remote],
697 error_ok=True).strip()
698 if tag_spec:
699 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
700 if self.svn_branch:
701 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702
703 if not self.svn_branch:
704 DieWithError('Can\'t guess svn branch -- try specifying it on the '
705 'command line')
706
707 return self.svn_branch
708
709 def GetTreeStatusUrl(self, error_ok=False):
710 if not self.tree_status_url:
711 error_message = ('You must configure your tree status URL by running '
712 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000713 self.tree_status_url = self._GetRietveldConfig(
714 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 return self.tree_status_url
716
717 def GetViewVCUrl(self):
718 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000719 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 return self.viewvc_url
721
rmistry@google.com90752582014-01-14 21:04:50 +0000722 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000724
rmistry@google.com78948ed2015-07-08 23:09:57 +0000725 def GetIsSkipDependencyUpload(self, branch_name):
726 """Returns true if specified branch should skip dep uploads."""
727 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
728 error_ok=True)
729
rmistry@google.com5626a922015-02-26 14:03:30 +0000730 def GetRunPostUploadHook(self):
731 run_post_upload_hook = self._GetRietveldConfig(
732 'run-post-upload-hook', error_ok=True)
733 return run_post_upload_hook == "True"
734
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000735 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000737
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000738 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000739 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000740
ukai@chromium.orge8077812012-02-03 03:41:46 +0000741 def GetIsGerrit(self):
742 """Return true if this repo is assosiated with gerrit code review system."""
743 if self.is_gerrit is None:
744 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
745 return self.is_gerrit
746
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000747 def GetSquashGerritUploads(self):
748 """Return true if uploads to Gerrit should be squashed by default."""
749 if self.squash_gerrit_uploads is None:
750 self.squash_gerrit_uploads = (
751 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
752 error_ok=True).strip() == 'true')
753 return self.squash_gerrit_uploads
754
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000755 def GetGitEditor(self):
756 """Return the editor specified in the git config, or None if none is."""
757 if self.git_editor is None:
758 self.git_editor = self._GetConfig('core.editor', error_ok=True)
759 return self.git_editor or None
760
thestig@chromium.org44202a22014-03-11 19:22:18 +0000761 def GetLintRegex(self):
762 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
763 DEFAULT_LINT_REGEX)
764
765 def GetLintIgnoreRegex(self):
766 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
767 DEFAULT_LINT_IGNORE_REGEX)
768
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000769 def GetProject(self):
770 if not self.project:
771 self.project = self._GetRietveldConfig('project', error_ok=True)
772 return self.project
773
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000774 def GetForceHttpsCommitUrl(self):
775 if not self.force_https_commit_url:
776 self.force_https_commit_url = self._GetRietveldConfig(
777 'force-https-commit-url', error_ok=True)
778 return self.force_https_commit_url
779
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000780 def GetPendingRefPrefix(self):
781 if not self.pending_ref_prefix:
782 self.pending_ref_prefix = self._GetRietveldConfig(
783 'pending-ref-prefix', error_ok=True)
784 return self.pending_ref_prefix
785
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 def _GetRietveldConfig(self, param, **kwargs):
787 return self._GetConfig('rietveld.' + param, **kwargs)
788
rmistry@google.com78948ed2015-07-08 23:09:57 +0000789 def _GetBranchConfig(self, branch_name, param, **kwargs):
790 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
791
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 def _GetConfig(self, param, **kwargs):
793 self.LazyUpdateIfNeeded()
794 return RunGit(['config', param], **kwargs).strip()
795
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797def ShortBranchName(branch):
798 """Convert a name like 'refs/heads/foo' to just 'foo'."""
799 return branch.replace('refs/heads/', '')
800
801
802class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000803 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000804 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000805 global settings
806 if not settings:
807 # Happens when git_cl.py is used as a utility library.
808 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 settings.GetDefaultServerUrl()
810 self.branchref = branchref
811 if self.branchref:
812 self.branch = ShortBranchName(self.branchref)
813 else:
814 self.branch = None
815 self.rietveld_server = None
816 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000817 self.lookedup_issue = False
818 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 self.has_description = False
820 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000821 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000823 self.cc = None
824 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000825 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000826 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000827 self._remote = None
828 self._rpc_server = None
829
830 @property
831 def auth_config(self):
832 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000833
834 def GetCCList(self):
835 """Return the users cc'd on this CL.
836
837 Return is a string suitable for passing to gcl with the --cc flag.
838 """
839 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000840 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000841 more_cc = ','.join(self.watchers)
842 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
843 return self.cc
844
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000845 def GetCCListWithoutDefault(self):
846 """Return the users cc'd on this CL excluding default ones."""
847 if self.cc is None:
848 self.cc = ','.join(self.watchers)
849 return self.cc
850
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000851 def SetWatchers(self, watchers):
852 """Set the list of email addresses that should be cc'd based on the changed
853 files in this CL.
854 """
855 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856
857 def GetBranch(self):
858 """Returns the short branch name, e.g. 'master'."""
859 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000860 branchref = RunGit(['symbolic-ref', 'HEAD'],
861 stderr=subprocess2.VOID, error_ok=True).strip()
862 if not branchref:
863 return None
864 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 self.branch = ShortBranchName(self.branchref)
866 return self.branch
867
868 def GetBranchRef(self):
869 """Returns the full branch name, e.g. 'refs/heads/master'."""
870 self.GetBranch() # Poke the lazy loader.
871 return self.branchref
872
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000873 @staticmethod
874 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000875 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 e.g. 'origin', 'refs/heads/master'
877 """
878 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
880 error_ok=True).strip()
881 if upstream_branch:
882 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
883 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000884 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
885 error_ok=True).strip()
886 if upstream_branch:
887 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000889 # Fall back on trying a git-svn upstream branch.
890 if settings.GetIsGitSvn():
891 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000893 # Else, try to guess the origin remote.
894 remote_branches = RunGit(['branch', '-r']).split()
895 if 'origin/master' in remote_branches:
896 # Fall back on origin/master if it exits.
897 remote = 'origin'
898 upstream_branch = 'refs/heads/master'
899 elif 'origin/trunk' in remote_branches:
900 # Fall back on origin/trunk if it exists. Generally a shared
901 # git-svn clone
902 remote = 'origin'
903 upstream_branch = 'refs/heads/trunk'
904 else:
905 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906Either pass complete "git diff"-style arguments, like
907 git cl upload origin/master
908or verify this branch is set up to track another (via the --track argument to
909"git checkout -b ...").""")
910
911 return remote, upstream_branch
912
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000913 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000914 upstream_branch = self.GetUpstreamBranch()
915 if not BranchExists(upstream_branch):
916 DieWithError('The upstream for the current branch (%s) does not exist '
917 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000918 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000919 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 def GetUpstreamBranch(self):
922 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000923 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000925 upstream_branch = upstream_branch.replace('refs/heads/',
926 'refs/remotes/%s/' % remote)
927 upstream_branch = upstream_branch.replace('refs/branch-heads/',
928 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 self.upstream_branch = upstream_branch
930 return self.upstream_branch
931
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000932 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000933 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000934 remote, branch = None, self.GetBranch()
935 seen_branches = set()
936 while branch not in seen_branches:
937 seen_branches.add(branch)
938 remote, branch = self.FetchUpstreamTuple(branch)
939 branch = ShortBranchName(branch)
940 if remote != '.' or branch.startswith('refs/remotes'):
941 break
942 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000943 remotes = RunGit(['remote'], error_ok=True).split()
944 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000945 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000946 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000947 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000948 logging.warning('Could not determine which remote this change is '
949 'associated with, so defaulting to "%s". This may '
950 'not be what you want. You may prevent this message '
951 'by running "git svn info" as documented here: %s',
952 self._remote,
953 GIT_INSTRUCTIONS_URL)
954 else:
955 logging.warn('Could not determine which remote this change is '
956 'associated with. You may prevent this message by '
957 'running "git svn info" as documented here: %s',
958 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000959 branch = 'HEAD'
960 if branch.startswith('refs/remotes'):
961 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000962 elif branch.startswith('refs/branch-heads/'):
963 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 else:
965 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000966 return self._remote
967
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000968 def GitSanityChecks(self, upstream_git_obj):
969 """Checks git repo status and ensures diff is from local commits."""
970
sbc@chromium.org79706062015-01-14 21:18:12 +0000971 if upstream_git_obj is None:
972 if self.GetBranch() is None:
973 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000974 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000975 else:
976 print >> sys.stderr, (
977 'ERROR: no upstream branch')
978 return False
979
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000980 # Verify the commit we're diffing against is in our current branch.
981 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
982 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
983 if upstream_sha != common_ancestor:
984 print >> sys.stderr, (
985 'ERROR: %s is not in the current branch. You may need to rebase '
986 'your tracking branch' % upstream_sha)
987 return False
988
989 # List the commits inside the diff, and verify they are all local.
990 commits_in_diff = RunGit(
991 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
992 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
993 remote_branch = remote_branch.strip()
994 if code != 0:
995 _, remote_branch = self.GetRemoteBranch()
996
997 commits_in_remote = RunGit(
998 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
999
1000 common_commits = set(commits_in_diff) & set(commits_in_remote)
1001 if common_commits:
1002 print >> sys.stderr, (
1003 'ERROR: Your diff contains %d commits already in %s.\n'
1004 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1005 'the diff. If you are using a custom git flow, you can override'
1006 ' the reference used for this check with "git config '
1007 'gitcl.remotebranch <git-ref>".' % (
1008 len(common_commits), remote_branch, upstream_git_obj))
1009 return False
1010 return True
1011
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001012 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001013 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001014
1015 Returns None if it is not set.
1016 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001017 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1018 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001019
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001020 def GetGitSvnRemoteUrl(self):
1021 """Return the configured git-svn remote URL parsed from git svn info.
1022
1023 Returns None if it is not set.
1024 """
1025 # URL is dependent on the current directory.
1026 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1027 if data:
1028 keys = dict(line.split(': ', 1) for line in data.splitlines()
1029 if ': ' in line)
1030 return keys.get('URL', None)
1031 return None
1032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 def GetRemoteUrl(self):
1034 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1035
1036 Returns None if there is no remote.
1037 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001038 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001039 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1040
1041 # If URL is pointing to a local directory, it is probably a git cache.
1042 if os.path.isdir(url):
1043 url = RunGit(['config', 'remote.%s.url' % remote],
1044 error_ok=True,
1045 cwd=url).strip()
1046 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047
1048 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001049 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001050 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001052 self.issue = int(issue) or None if issue else None
1053 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 return self.issue
1055
1056 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001057 if not self.rietveld_server:
1058 # If we're on a branch then get the server potentially associated
1059 # with that branch.
1060 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001061 rietveld_server_config = self._RietveldServer()
1062 if rietveld_server_config:
1063 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1064 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001065 if not self.rietveld_server:
1066 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 return self.rietveld_server
1068
1069 def GetIssueURL(self):
1070 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001071 if not self.GetIssue():
1072 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1074
1075 def GetDescription(self, pretty=False):
1076 if not self.has_description:
1077 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001078 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001079 try:
1080 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001081 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001082 if e.code == 404:
1083 DieWithError(
1084 ('\nWhile fetching the description for issue %d, received a '
1085 '404 (not found)\n'
1086 'error. It is likely that you deleted this '
1087 'issue on the server. If this is the\n'
1088 'case, please run\n\n'
1089 ' git cl issue 0\n\n'
1090 'to clear the association with the deleted issue. Then run '
1091 'this command again.') % issue)
1092 else:
1093 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001094 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001095 except urllib2.URLError as e:
1096 print >> sys.stderr, (
1097 'Warning: Failed to retrieve CL description due to network '
1098 'failure.')
1099 self.description = ''
1100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 self.has_description = True
1102 if pretty:
1103 wrapper = textwrap.TextWrapper()
1104 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1105 return wrapper.fill(self.description)
1106 return self.description
1107
1108 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001109 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001110 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 patchset = RunGit(['config', self._PatchsetSetting()],
1112 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001113 self.patchset = int(patchset) or None if patchset else None
1114 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 return self.patchset
1116
1117 def SetPatchset(self, patchset):
1118 """Set this branch's patchset. If patchset=0, clears the patchset."""
1119 if patchset:
1120 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001121 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 else:
1123 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001124 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001125 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 def GetMostRecentPatchset(self):
1128 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001129
1130 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001131 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001132 '/download/issue%s_%s.diff' % (issue, patchset))
1133
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001134 def GetIssueProperties(self):
1135 if self._props is None:
1136 issue = self.GetIssue()
1137 if not issue:
1138 self._props = {}
1139 else:
1140 self._props = self.RpcServer().get_issue_properties(issue, True)
1141 return self._props
1142
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001143 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001145
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001146 def AddComment(self, message):
1147 return self.RpcServer().add_comment(self.GetIssue(), message)
1148
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 def SetIssue(self, issue):
1150 """Set this branch's issue. If issue=0, clears the issue."""
1151 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001152 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 RunGit(['config', self._IssueSetting(), str(issue)])
1154 if self.rietveld_server:
1155 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1156 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001157 current_issue = self.GetIssue()
1158 if current_issue:
1159 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 self.issue = None
1161 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001163 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001164 if not self.GitSanityChecks(upstream_branch):
1165 DieWithError('\nGit sanity check failure')
1166
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001167 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001168 if not root:
1169 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001170 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001171
1172 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001173 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001174 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001175 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001176 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001177 except subprocess2.CalledProcessError:
1178 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001179 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 'This branch probably doesn\'t exist anymore. To reset the\n'
1181 'tracking branch, please run\n'
1182 ' git branch --set-upstream %s trunk\n'
1183 'replacing trunk with origin/master or the relevant branch') %
1184 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001185
maruel@chromium.org52424302012-08-29 15:14:30 +00001186 issue = self.GetIssue()
1187 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188 if issue:
1189 description = self.GetDescription()
1190 else:
1191 # If the change was never uploaded, use the log messages of all commits
1192 # up to the branch point, as git cl upload will prefill the description
1193 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001194 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1195 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001196
1197 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001198 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001199 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001200 name,
1201 description,
1202 absroot,
1203 files,
1204 issue,
1205 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001206 author,
1207 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001208
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001209 def GetStatus(self):
1210 """Apply a rough heuristic to give a simple summary of an issue's review
1211 or CQ status, assuming adherence to a common workflow.
1212
1213 Returns None if no issue for this branch, or one of the following keywords:
1214 * 'error' - error from review tool (including deleted issues)
1215 * 'unsent' - not sent for review
1216 * 'waiting' - waiting for review
1217 * 'reply' - waiting for owner to reply to review
1218 * 'lgtm' - LGTM from at least one approved reviewer
1219 * 'commit' - in the commit queue
1220 * 'closed' - closed
1221 """
1222 if not self.GetIssue():
1223 return None
1224
1225 try:
1226 props = self.GetIssueProperties()
1227 except urllib2.HTTPError:
1228 return 'error'
1229
1230 if props.get('closed'):
1231 # Issue is closed.
1232 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001233 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001234 # Issue is in the commit queue.
1235 return 'commit'
1236
1237 try:
1238 reviewers = self.GetApprovingReviewers()
1239 except urllib2.HTTPError:
1240 return 'error'
1241
1242 if reviewers:
1243 # Was LGTM'ed.
1244 return 'lgtm'
1245
1246 messages = props.get('messages') or []
1247
1248 if not messages:
1249 # No message was sent.
1250 return 'unsent'
1251 if messages[-1]['sender'] != props.get('owner_email'):
1252 # Non-LGTM reply from non-owner
1253 return 'reply'
1254 return 'waiting'
1255
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001256 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001257 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001258
1259 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001260 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001262 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001263 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001264 except presubmit_support.PresubmitFailure, e:
1265 DieWithError(
1266 ('%s\nMaybe your depot_tools is out of date?\n'
1267 'If all fails, contact maruel@') % e)
1268
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001269 def UpdateDescription(self, description):
1270 self.description = description
1271 return self.RpcServer().update_description(
1272 self.GetIssue(), self.description)
1273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001275 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001276 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001278 def SetFlag(self, flag, value):
1279 """Patchset must match."""
1280 if not self.GetPatchset():
1281 DieWithError('The patchset needs to match. Send another patchset.')
1282 try:
1283 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001284 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001285 except urllib2.HTTPError, e:
1286 if e.code == 404:
1287 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1288 if e.code == 403:
1289 DieWithError(
1290 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1291 'match?') % (self.GetIssue(), self.GetPatchset()))
1292 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001294 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 """Returns an upload.RpcServer() to access this review's rietveld instance.
1296 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001297 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001298 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001299 self.GetRietveldServer(),
1300 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001301 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302
1303 def _IssueSetting(self):
1304 """Return the git setting that stores this change's issue."""
1305 return 'branch.%s.rietveldissue' % self.GetBranch()
1306
1307 def _PatchsetSetting(self):
1308 """Return the git setting that stores this change's most recent patchset."""
1309 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1310
1311 def _RietveldServer(self):
1312 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001313 branch = self.GetBranch()
1314 if branch:
1315 return 'branch.%s.rietveldserver' % branch
1316 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317
1318
1319def GetCodereviewSettingsInteractively():
1320 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001321 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 server = settings.GetDefaultServerUrl(error_ok=True)
1323 prompt = 'Rietveld server (host[:port])'
1324 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001325 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326 if not server and not newserver:
1327 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001328 if newserver:
1329 newserver = gclient_utils.UpgradeToHttps(newserver)
1330 if newserver != server:
1331 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001333 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 prompt = caption
1335 if initial:
1336 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001337 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 if new_val == 'x':
1339 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001340 elif new_val:
1341 if is_url:
1342 new_val = gclient_utils.UpgradeToHttps(new_val)
1343 if new_val != initial:
1344 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001346 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001347 SetProperty(settings.GetDefaultPrivateFlag(),
1348 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001350 'tree-status-url', False)
1351 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001352 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001353 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1354 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
1356 # TODO: configure a default branch to diff against, rather than this
1357 # svn-based hackery.
1358
1359
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001360class ChangeDescription(object):
1361 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001362 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001363 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001364
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001365 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001366 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001367
agable@chromium.org42c20792013-09-12 17:34:49 +00001368 @property # www.logilab.org/ticket/89786
1369 def description(self): # pylint: disable=E0202
1370 return '\n'.join(self._description_lines)
1371
1372 def set_description(self, desc):
1373 if isinstance(desc, basestring):
1374 lines = desc.splitlines()
1375 else:
1376 lines = [line.rstrip() for line in desc]
1377 while lines and not lines[0]:
1378 lines.pop(0)
1379 while lines and not lines[-1]:
1380 lines.pop(-1)
1381 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001382
piman@chromium.org336f9122014-09-04 02:16:55 +00001383 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001384 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001385 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001386 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001387 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001388 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001389
agable@chromium.org42c20792013-09-12 17:34:49 +00001390 # Get the set of R= and TBR= lines and remove them from the desciption.
1391 regexp = re.compile(self.R_LINE)
1392 matches = [regexp.match(line) for line in self._description_lines]
1393 new_desc = [l for i, l in enumerate(self._description_lines)
1394 if not matches[i]]
1395 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001396
agable@chromium.org42c20792013-09-12 17:34:49 +00001397 # Construct new unified R= and TBR= lines.
1398 r_names = []
1399 tbr_names = []
1400 for match in matches:
1401 if not match:
1402 continue
1403 people = cleanup_list([match.group(2).strip()])
1404 if match.group(1) == 'TBR':
1405 tbr_names.extend(people)
1406 else:
1407 r_names.extend(people)
1408 for name in r_names:
1409 if name not in reviewers:
1410 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001411 if add_owners_tbr:
1412 owners_db = owners.Database(change.RepositoryRoot(),
1413 fopen=file, os_path=os.path, glob=glob.glob)
1414 all_reviewers = set(tbr_names + reviewers)
1415 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1416 all_reviewers)
1417 tbr_names.extend(owners_db.reviewers_for(missing_files,
1418 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001419 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1420 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1421
1422 # Put the new lines in the description where the old first R= line was.
1423 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1424 if 0 <= line_loc < len(self._description_lines):
1425 if new_tbr_line:
1426 self._description_lines.insert(line_loc, new_tbr_line)
1427 if new_r_line:
1428 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001430 if new_r_line:
1431 self.append_footer(new_r_line)
1432 if new_tbr_line:
1433 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001434
1435 def prompt(self):
1436 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001437 self.set_description([
1438 '# Enter a description of the change.',
1439 '# This will be displayed on the codereview site.',
1440 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001441 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001442 '--------------------',
1443 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001444
agable@chromium.org42c20792013-09-12 17:34:49 +00001445 regexp = re.compile(self.BUG_LINE)
1446 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001447 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001448 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001449 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001450 if not content:
1451 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001452 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001453
1454 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001455 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1456 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001457 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001458 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001459
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001460 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001461 if self._description_lines:
1462 # Add an empty line if either the last line or the new line isn't a tag.
1463 last_line = self._description_lines[-1]
1464 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1465 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1466 self._description_lines.append('')
1467 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001468
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001469 def get_reviewers(self):
1470 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001471 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1472 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001474
1475
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001476def get_approving_reviewers(props):
1477 """Retrieves the reviewers that approved a CL from the issue properties with
1478 messages.
1479
1480 Note that the list may contain reviewers that are not committer, thus are not
1481 considered by the CQ.
1482 """
1483 return sorted(
1484 set(
1485 message['sender']
1486 for message in props['messages']
1487 if message['approval'] and message['sender'] in props['reviewers']
1488 )
1489 )
1490
1491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492def FindCodereviewSettingsFile(filename='codereview.settings'):
1493 """Finds the given file starting in the cwd and going up.
1494
1495 Only looks up to the top of the repository unless an
1496 'inherit-review-settings-ok' file exists in the root of the repository.
1497 """
1498 inherit_ok_file = 'inherit-review-settings-ok'
1499 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001500 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1502 root = '/'
1503 while True:
1504 if filename in os.listdir(cwd):
1505 if os.path.isfile(os.path.join(cwd, filename)):
1506 return open(os.path.join(cwd, filename))
1507 if cwd == root:
1508 break
1509 cwd = os.path.dirname(cwd)
1510
1511
1512def LoadCodereviewSettingsFromFile(fileobj):
1513 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001514 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 def SetProperty(name, setting, unset_error_ok=False):
1517 fullname = 'rietveld.' + name
1518 if setting in keyvals:
1519 RunGit(['config', fullname, keyvals[setting]])
1520 else:
1521 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1522
1523 SetProperty('server', 'CODE_REVIEW_SERVER')
1524 # Only server setting is required. Other settings can be absent.
1525 # In that case, we ignore errors raised during option deletion attempt.
1526 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001527 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1529 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001530 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001531 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001532 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1533 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001534 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001535 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001536 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001537 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1538 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001540 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001541 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001542
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001543 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1544 RunGit(['config', 'gerrit.squash-uploads',
1545 keyvals['GERRIT_SQUASH_UPLOADS']])
1546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1548 #should be of the form
1549 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1550 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1551 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1552 keyvals['ORIGIN_URL_CONFIG']])
1553
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001555def urlretrieve(source, destination):
1556 """urllib is broken for SSL connections via a proxy therefore we
1557 can't use urllib.urlretrieve()."""
1558 with open(destination, 'w') as f:
1559 f.write(urllib2.urlopen(source).read())
1560
1561
ukai@chromium.org712d6102013-11-27 00:52:58 +00001562def hasSheBang(fname):
1563 """Checks fname is a #! script."""
1564 with open(fname) as f:
1565 return f.read(2).startswith('#!')
1566
1567
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001568def DownloadGerritHook(force):
1569 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001570
1571 Args:
1572 force: True to update hooks. False to install hooks if not present.
1573 """
1574 if not settings.GetIsGerrit():
1575 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001576 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001577 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1578 if not os.access(dst, os.X_OK):
1579 if os.path.exists(dst):
1580 if not force:
1581 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001582 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001583 print(
1584 'WARNING: installing Gerrit commit-msg hook.\n'
1585 ' This behavior of git cl will soon be disabled.\n'
1586 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001587 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001588 if not hasSheBang(dst):
1589 DieWithError('Not a script: %s\n'
1590 'You need to download from\n%s\n'
1591 'into .git/hooks/commit-msg and '
1592 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001593 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1594 except Exception:
1595 if os.path.exists(dst):
1596 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001597 DieWithError('\nFailed to download hooks.\n'
1598 'You need to download from\n%s\n'
1599 'into .git/hooks/commit-msg and '
1600 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001601
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001602# TODO(tandrii): remove this once repos which call this method directly are
tandrii@chromium.org675bec32016-03-04 16:36:58 +00001603# upgraded. See http://crbug.com/579176.
tandrii@chromium.orgc55295c2016-03-04 15:54:59 +00001604DownloadHooks = DownloadGerritHook
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001605
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001606@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001608 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001610 parser.add_option('--activate-update', action='store_true',
1611 help='activate auto-updating [rietveld] section in '
1612 '.git/config')
1613 parser.add_option('--deactivate-update', action='store_true',
1614 help='deactivate auto-updating [rietveld] section in '
1615 '.git/config')
1616 options, args = parser.parse_args(args)
1617
1618 if options.deactivate_update:
1619 RunGit(['config', 'rietveld.autoupdate', 'false'])
1620 return
1621
1622 if options.activate_update:
1623 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1624 return
1625
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626 if len(args) == 0:
1627 GetCodereviewSettingsInteractively()
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001628 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 return 0
1630
1631 url = args[0]
1632 if not url.endswith('codereview.settings'):
1633 url = os.path.join(url, 'codereview.settings')
1634
1635 # Load code review settings and download hooks (if available).
1636 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001637 DownloadGerritHook(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638 return 0
1639
1640
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001641def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001642 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001643 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1644 branch = ShortBranchName(branchref)
1645 _, args = parser.parse_args(args)
1646 if not args:
1647 print("Current base-url:")
1648 return RunGit(['config', 'branch.%s.base-url' % branch],
1649 error_ok=False).strip()
1650 else:
1651 print("Setting base-url to %s" % args[0])
1652 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1653 error_ok=False).strip()
1654
1655
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001656def color_for_status(status):
1657 """Maps a Changelist status to color, for CMDstatus and other tools."""
1658 return {
1659 'unsent': Fore.RED,
1660 'waiting': Fore.BLUE,
1661 'reply': Fore.YELLOW,
1662 'lgtm': Fore.GREEN,
1663 'commit': Fore.MAGENTA,
1664 'closed': Fore.CYAN,
1665 'error': Fore.WHITE,
1666 }.get(status, Fore.WHITE)
1667
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001668def fetch_cl_status(branch, auth_config=None):
1669 """Fetches information for an issue and returns (branch, issue, status)."""
1670 cl = Changelist(branchref=branch, auth_config=auth_config)
1671 url = cl.GetIssueURL()
1672 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001673
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001674 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001676 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001677
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001678 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001679
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001680def get_cl_statuses(
1681 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001682 """Returns a blocking iterable of (branch, issue, color) for given branches.
1683
1684 If fine_grained is true, this will fetch CL statuses from the server.
1685 Otherwise, simply indicate if there's a matching url for the given branches.
1686
1687 If max_processes is specified, it is used as the maximum number of processes
1688 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1689 spawned.
1690 """
1691 # Silence upload.py otherwise it becomes unwieldly.
1692 upload.verbosity = 0
1693
1694 if fine_grained:
1695 # Process one branch synchronously to work through authentication, then
1696 # spawn processes to process all the other branches in parallel.
1697 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001698 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1699 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001700
1701 branches_to_fetch = branches[1:]
1702 pool = ThreadPool(
1703 min(max_processes, len(branches_to_fetch))
1704 if max_processes is not None
1705 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001706 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001707 yield x
1708 else:
1709 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1710 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001711 cl = Changelist(branchref=b, auth_config=auth_config)
1712 url = cl.GetIssueURL()
1713 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001714
rmistry@google.com2dd99862015-06-22 12:22:18 +00001715
1716def upload_branch_deps(cl, args):
1717 """Uploads CLs of local branches that are dependents of the current branch.
1718
1719 If the local branch dependency tree looks like:
1720 test1 -> test2.1 -> test3.1
1721 -> test3.2
1722 -> test2.2 -> test3.3
1723
1724 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1725 run on the dependent branches in this order:
1726 test2.1, test3.1, test3.2, test2.2, test3.3
1727
1728 Note: This function does not rebase your local dependent branches. Use it when
1729 you make a change to the parent branch that will not conflict with its
1730 dependent branches, and you would like their dependencies updated in
1731 Rietveld.
1732 """
1733 if git_common.is_dirty_git_tree('upload-branch-deps'):
1734 return 1
1735
1736 root_branch = cl.GetBranch()
1737 if root_branch is None:
1738 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1739 'Get on a branch!')
1740 if not cl.GetIssue() or not cl.GetPatchset():
1741 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1742 'patchset dependencies without an uploaded CL.')
1743
1744 branches = RunGit(['for-each-ref',
1745 '--format=%(refname:short) %(upstream:short)',
1746 'refs/heads'])
1747 if not branches:
1748 print('No local branches found.')
1749 return 0
1750
1751 # Create a dictionary of all local branches to the branches that are dependent
1752 # on it.
1753 tracked_to_dependents = collections.defaultdict(list)
1754 for b in branches.splitlines():
1755 tokens = b.split()
1756 if len(tokens) == 2:
1757 branch_name, tracked = tokens
1758 tracked_to_dependents[tracked].append(branch_name)
1759
1760 print
1761 print 'The dependent local branches of %s are:' % root_branch
1762 dependents = []
1763 def traverse_dependents_preorder(branch, padding=''):
1764 dependents_to_process = tracked_to_dependents.get(branch, [])
1765 padding += ' '
1766 for dependent in dependents_to_process:
1767 print '%s%s' % (padding, dependent)
1768 dependents.append(dependent)
1769 traverse_dependents_preorder(dependent, padding)
1770 traverse_dependents_preorder(root_branch)
1771 print
1772
1773 if not dependents:
1774 print 'There are no dependent local branches for %s' % root_branch
1775 return 0
1776
1777 print ('This command will checkout all dependent branches and run '
1778 '"git cl upload".')
1779 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1780
andybons@chromium.org962f9462016-02-03 20:00:42 +00001781 # Add a default patchset title to all upload calls in Rietveld.
1782 if not settings.GetIsGerrit():
1783 args.extend(['-t', 'Updated patchset dependency'])
1784
rmistry@google.com2dd99862015-06-22 12:22:18 +00001785 # Record all dependents that failed to upload.
1786 failures = {}
1787 # Go through all dependents, checkout the branch and upload.
1788 try:
1789 for dependent_branch in dependents:
1790 print
1791 print '--------------------------------------'
1792 print 'Running "git cl upload" from %s:' % dependent_branch
1793 RunGit(['checkout', '-q', dependent_branch])
1794 print
1795 try:
1796 if CMDupload(OptionParser(), args) != 0:
1797 print 'Upload failed for %s!' % dependent_branch
1798 failures[dependent_branch] = 1
1799 except: # pylint: disable=W0702
1800 failures[dependent_branch] = 1
1801 print
1802 finally:
1803 # Swap back to the original root branch.
1804 RunGit(['checkout', '-q', root_branch])
1805
1806 print
1807 print 'Upload complete for dependent branches!'
1808 for dependent_branch in dependents:
1809 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1810 print ' %s : %s' % (dependent_branch, upload_status)
1811 print
1812
1813 return 0
1814
1815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001817 """Show status of changelists.
1818
1819 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001820 - Red not sent for review or broken
1821 - Blue waiting for review
1822 - Yellow waiting for you to reply to review
1823 - Green LGTM'ed
1824 - Magenta in the commit queue
1825 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001826
1827 Also see 'git cl comments'.
1828 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 parser.add_option('--field',
1830 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001831 parser.add_option('-f', '--fast', action='store_true',
1832 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001833 parser.add_option(
1834 '-j', '--maxjobs', action='store', type=int,
1835 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001836
1837 auth.add_auth_options(parser)
1838 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001839 if args:
1840 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001841 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001844 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 if options.field.startswith('desc'):
1846 print cl.GetDescription()
1847 elif options.field == 'id':
1848 issueid = cl.GetIssue()
1849 if issueid:
1850 print issueid
1851 elif options.field == 'patch':
1852 patchset = cl.GetPatchset()
1853 if patchset:
1854 print patchset
1855 elif options.field == 'url':
1856 url = cl.GetIssueURL()
1857 if url:
1858 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001859 return 0
1860
1861 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1862 if not branches:
1863 print('No local branch found.')
1864 return 0
1865
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001866 changes = (
1867 Changelist(branchref=b, auth_config=auth_config)
1868 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001869 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001870 alignment = max(5, max(len(b) for b in branches))
1871 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001872 output = get_cl_statuses(branches,
1873 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001874 max_processes=options.maxjobs,
1875 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001876
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001877 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001878 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001879 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001880 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001881 b, i, status = output.next()
1882 branch_statuses[b] = (i, status)
1883 issue_url, status = branch_statuses.pop(branch)
1884 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001885 reset = Fore.RESET
1886 if not sys.stdout.isatty():
1887 color = ''
1888 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001889 status_str = '(%s)' % status if status else ''
1890 print ' %*s : %s%s %s%s' % (
1891 alignment, ShortBranchName(branch), color, issue_url, status_str,
1892 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001893
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001894 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001895 print
1896 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001897 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001898 if not cl.GetIssue():
1899 print 'No issue assigned.'
1900 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001901 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001902 if not options.fast:
1903 print 'Issue description:'
1904 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 return 0
1906
1907
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001908def colorize_CMDstatus_doc():
1909 """To be called once in main() to add colors to git cl status help."""
1910 colors = [i for i in dir(Fore) if i[0].isupper()]
1911
1912 def colorize_line(line):
1913 for color in colors:
1914 if color in line.upper():
1915 # Extract whitespaces first and the leading '-'.
1916 indent = len(line) - len(line.lstrip(' ')) + 1
1917 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1918 return line
1919
1920 lines = CMDstatus.__doc__.splitlines()
1921 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1922
1923
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001924@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001926 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927
1928 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001929 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001930 parser.add_option('-r', '--reverse', action='store_true',
1931 help='Lookup the branch(es) for the specified issues. If '
1932 'no issues are specified, all branches with mapped '
1933 'issues will be listed.')
1934 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935
dnj@chromium.org406c4402015-03-03 17:22:28 +00001936 if options.reverse:
1937 branches = RunGit(['for-each-ref', 'refs/heads',
1938 '--format=%(refname:short)']).splitlines()
1939
1940 # Reverse issue lookup.
1941 issue_branch_map = {}
1942 for branch in branches:
1943 cl = Changelist(branchref=branch)
1944 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1945 if not args:
1946 args = sorted(issue_branch_map.iterkeys())
1947 for issue in args:
1948 if not issue:
1949 continue
1950 print 'Branch for issue number %s: %s' % (
1951 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1952 else:
1953 cl = Changelist()
1954 if len(args) > 0:
1955 try:
1956 issue = int(args[0])
1957 except ValueError:
1958 DieWithError('Pass a number to set the issue or none to list it.\n'
1959 'Maybe you want to run git cl status?')
1960 cl.SetIssue(issue)
1961 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 return 0
1963
1964
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001965def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001966 """Shows or posts review comments for any changelist."""
1967 parser.add_option('-a', '--add-comment', dest='comment',
1968 help='comment to add to an issue')
1969 parser.add_option('-i', dest='issue',
1970 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001971 parser.add_option('-j', '--json-file',
1972 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001973 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001974 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001975 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001976
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001977 issue = None
1978 if options.issue:
1979 try:
1980 issue = int(options.issue)
1981 except ValueError:
1982 DieWithError('A review issue id is expected to be a number')
1983
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001984 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001985
1986 if options.comment:
1987 cl.AddComment(options.comment)
1988 return 0
1989
1990 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001991 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001992 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001993 summary.append({
1994 'date': message['date'],
1995 'lgtm': False,
1996 'message': message['text'],
1997 'not_lgtm': False,
1998 'sender': message['sender'],
1999 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002000 if message['disapproval']:
2001 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002002 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002003 elif message['approval']:
2004 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002005 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002006 elif message['sender'] == data['owner_email']:
2007 color = Fore.MAGENTA
2008 else:
2009 color = Fore.BLUE
2010 print '\n%s%s %s%s' % (
2011 color, message['date'].split('.', 1)[0], message['sender'],
2012 Fore.RESET)
2013 if message['text'].strip():
2014 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002015 if options.json_file:
2016 with open(options.json_file, 'wb') as f:
2017 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002018 return 0
2019
2020
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002021def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002022 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002023 parser.add_option('-d', '--display', action='store_true',
2024 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002025 auth.add_auth_options(parser)
2026 options, _ = parser.parse_args(args)
2027 auth_config = auth.extract_auth_config_from_options(options)
2028 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002029 if not cl.GetIssue():
2030 DieWithError('This branch has no associated changelist.')
2031 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002032 if options.display:
2033 print description.description
2034 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002035 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002036 if cl.GetDescription() != description.description:
2037 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002038 return 0
2039
2040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041def CreateDescriptionFromLog(args):
2042 """Pulls out the commit log to use as a base for the CL description."""
2043 log_args = []
2044 if len(args) == 1 and not args[0].endswith('.'):
2045 log_args = [args[0] + '..']
2046 elif len(args) == 1 and args[0].endswith('...'):
2047 log_args = [args[0][:-1]]
2048 elif len(args) == 2:
2049 log_args = [args[0] + '..' + args[1]]
2050 else:
2051 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002052 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053
2054
thestig@chromium.org44202a22014-03-11 19:22:18 +00002055def CMDlint(parser, args):
2056 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002057 parser.add_option('--filter', action='append', metavar='-x,+y',
2058 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002059 auth.add_auth_options(parser)
2060 options, args = parser.parse_args(args)
2061 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002062
2063 # Access to a protected member _XX of a client class
2064 # pylint: disable=W0212
2065 try:
2066 import cpplint
2067 import cpplint_chromium
2068 except ImportError:
2069 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2070 return 1
2071
2072 # Change the current working directory before calling lint so that it
2073 # shows the correct base.
2074 previous_cwd = os.getcwd()
2075 os.chdir(settings.GetRoot())
2076 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002077 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002078 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2079 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002080 if not files:
2081 print "Cannot lint an empty CL"
2082 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002083
2084 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002085 command = args + files
2086 if options.filter:
2087 command = ['--filter=' + ','.join(options.filter)] + command
2088 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002089
2090 white_regex = re.compile(settings.GetLintRegex())
2091 black_regex = re.compile(settings.GetLintIgnoreRegex())
2092 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2093 for filename in filenames:
2094 if white_regex.match(filename):
2095 if black_regex.match(filename):
2096 print "Ignoring file %s" % filename
2097 else:
2098 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2099 extra_check_functions)
2100 else:
2101 print "Skipping file %s" % filename
2102 finally:
2103 os.chdir(previous_cwd)
2104 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2105 if cpplint._cpplint_state.error_count != 0:
2106 return 1
2107 return 0
2108
2109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002111 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002112 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002114 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002115 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002116 auth.add_auth_options(parser)
2117 options, args = parser.parse_args(args)
2118 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119
sbc@chromium.org71437c02015-04-09 19:29:40 +00002120 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002121 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 return 1
2123
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002124 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 if args:
2126 base_branch = args[0]
2127 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002128 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002129 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002131 cl.RunHook(
2132 committing=not options.upload,
2133 may_prompt=False,
2134 verbose=options.verbose,
2135 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002136 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137
2138
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002139def AddChangeIdToCommitMessage(options, args):
2140 """Re-commits using the current message, assumes the commit hook is in
2141 place.
2142 """
2143 log_desc = options.message or CreateDescriptionFromLog(args)
2144 git_command = ['commit', '--amend', '-m', log_desc]
2145 RunGit(git_command)
2146 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002147 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002148 print 'git-cl: Added Change-Id to commit message.'
2149 else:
2150 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2151
2152
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002153def GenerateGerritChangeId(message):
2154 """Returns Ixxxxxx...xxx change id.
2155
2156 Works the same way as
2157 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2158 but can be called on demand on all platforms.
2159
2160 The basic idea is to generate git hash of a state of the tree, original commit
2161 message, author/committer info and timestamps.
2162 """
2163 lines = []
2164 tree_hash = RunGitSilent(['write-tree'])
2165 lines.append('tree %s' % tree_hash.strip())
2166 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2167 if code == 0:
2168 lines.append('parent %s' % parent.strip())
2169 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2170 lines.append('author %s' % author.strip())
2171 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2172 lines.append('committer %s' % committer.strip())
2173 lines.append('')
2174 # Note: Gerrit's commit-hook actually cleans message of some lines and
2175 # whitespace. This code is not doing this, but it clearly won't decrease
2176 # entropy.
2177 lines.append(message)
2178 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2179 stdin='\n'.join(lines))
2180 return 'I%s' % change_hash.strip()
2181
2182
piman@chromium.org336f9122014-09-04 02:16:55 +00002183def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002184 """upload the current branch to gerrit."""
2185 # We assume the remote called "origin" is the one we want.
2186 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002187 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002188
2189 remote, remote_branch = cl.GetRemoteBranch()
2190 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2191 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002193 change_desc = ChangeDescription(
2194 options.message or CreateDescriptionFromLog(args))
2195 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002196 print "\nDescription is empty. Aborting..."
2197 return 1
2198
2199 if options.title:
2200 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002202
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002203 if options.squash:
2204 # Try to get the message from a previous upload.
2205 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002206 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002207 if not message:
2208 if not options.force:
2209 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002210 if not change_desc.description:
2211 print "Description is empty; aborting."
2212 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002213 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002214 change_ids = git_footers.get_footer_change_id(message)
2215 if len(change_ids) > 1:
2216 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2217 if not change_ids:
2218 message = git_footers.add_footer_change_id(
2219 message, GenerateGerritChangeId(message))
2220 change_desc.set_description(message)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002221
2222 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2223 if remote is '.':
2224 # If our upstream branch is local, we base our squashed commit on its
2225 # squashed version.
2226 parent = ('refs/heads/git_cl_uploads/' +
2227 scm.GIT.ShortBranchName(upstream_branch))
2228
2229 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2230 # will create additional CLs when uploading.
2231 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2232 RunGitSilent(['rev-parse', parent + ':'])):
2233 print 'Upload upstream branch ' + upstream_branch + ' first.'
2234 return 1
2235 else:
2236 parent = cl.GetCommonAncestorWithUpstream()
2237
2238 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2239 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2240 '-m', message]).strip()
2241 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002242 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002243 DownloadGerritHook(False)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002244 AddChangeIdToCommitMessage(options, args)
2245 ref_to_push = 'HEAD'
2246 parent = '%s/%s' % (gerrit_remote, branch)
2247
2248 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2249 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002250 if len(commits) > 1:
2251 print('WARNING: This will upload %d commits. Run the following command '
2252 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002253 print('git log %s..%s' % (parent, ref_to_push))
2254 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002255 'commit.')
2256 ask_for_data('About to upload; enter to confirm.')
2257
piman@chromium.org336f9122014-09-04 02:16:55 +00002258 if options.reviewers or options.tbr_owners:
2259 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260
ukai@chromium.orge8077812012-02-03 03:41:46 +00002261 receive_options = []
2262 cc = cl.GetCCList().split(',')
2263 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002264 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002265 cc = filter(None, cc)
2266 if cc:
2267 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002268 if change_desc.get_reviewers():
2269 receive_options.extend(
2270 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002271
ukai@chromium.orge8077812012-02-03 03:41:46 +00002272 git_command = ['push']
2273 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002274 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002275 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002276 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002277 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002278
2279 if options.squash:
2280 head = RunGit(['rev-parse', 'HEAD']).strip()
2281 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2282
ukai@chromium.orge8077812012-02-03 03:41:46 +00002283 # TODO(ukai): parse Change-Id: and set issue number?
2284 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002286
wittman@chromium.org455dc922015-01-26 20:15:50 +00002287def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2288 """Computes the remote branch ref to use for the CL.
2289
2290 Args:
2291 remote (str): The git remote for the CL.
2292 remote_branch (str): The git remote branch for the CL.
2293 target_branch (str): The target branch specified by the user.
2294 pending_prefix (str): The pending prefix from the settings.
2295 """
2296 if not (remote and remote_branch):
2297 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002298
wittman@chromium.org455dc922015-01-26 20:15:50 +00002299 if target_branch:
2300 # Cannonicalize branch references to the equivalent local full symbolic
2301 # refs, which are then translated into the remote full symbolic refs
2302 # below.
2303 if '/' not in target_branch:
2304 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2305 else:
2306 prefix_replacements = (
2307 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2308 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2309 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2310 )
2311 match = None
2312 for regex, replacement in prefix_replacements:
2313 match = re.search(regex, target_branch)
2314 if match:
2315 remote_branch = target_branch.replace(match.group(0), replacement)
2316 break
2317 if not match:
2318 # This is a branch path but not one we recognize; use as-is.
2319 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002320 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2321 # Handle the refs that need to land in different refs.
2322 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002323
wittman@chromium.org455dc922015-01-26 20:15:50 +00002324 # Create the true path to the remote branch.
2325 # Does the following translation:
2326 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2327 # * refs/remotes/origin/master -> refs/heads/master
2328 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2329 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2330 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2331 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2332 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2333 'refs/heads/')
2334 elif remote_branch.startswith('refs/remotes/branch-heads'):
2335 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2336 # If a pending prefix exists then replace refs/ with it.
2337 if pending_prefix:
2338 remote_branch = remote_branch.replace('refs/', pending_prefix)
2339 return remote_branch
2340
2341
piman@chromium.org336f9122014-09-04 02:16:55 +00002342def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002343 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002344 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2345 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002346 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002347 if options.emulate_svn_auto_props:
2348 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349
2350 change_desc = None
2351
pgervais@chromium.org91141372014-01-09 23:27:20 +00002352 if options.email is not None:
2353 upload_args.extend(['--email', options.email])
2354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002356 if options.title:
2357 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002358 if options.message:
2359 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002360 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361 print ("This branch is associated with issue %s. "
2362 "Adding patch to that issue." % cl.GetIssue())
2363 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002364 if options.title:
2365 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002366 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002367 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002368 if options.reviewers or options.tbr_owners:
2369 change_desc.update_reviewers(options.reviewers,
2370 options.tbr_owners,
2371 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002372 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002373 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002374
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002375 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376 print "Description is empty; aborting."
2377 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002378
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002379 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002380 if change_desc.get_reviewers():
2381 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002382 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002383 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002384 DieWithError("Must specify reviewers to send email.")
2385 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002386
2387 # We check this before applying rietveld.private assuming that in
2388 # rietveld.cc only addresses which we can send private CLs to are listed
2389 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2390 # --private is specified explicitly on the command line.
2391 if options.private:
2392 logging.warn('rietveld.cc is ignored since private flag is specified. '
2393 'You need to review and add them manually if necessary.')
2394 cc = cl.GetCCListWithoutDefault()
2395 else:
2396 cc = cl.GetCCList()
2397 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002398 if cc:
2399 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002401 if options.private or settings.GetDefaultPrivateFlag() == "True":
2402 upload_args.append('--private')
2403
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002404 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002405 if not options.find_copies:
2406 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408 # Include the upstream repo's URL in the change -- this is useful for
2409 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002410 remote_url = cl.GetGitBaseUrlFromConfig()
2411 if not remote_url:
2412 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002413 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002414 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002415 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2416 remote_url = (cl.GetRemoteUrl() + '@'
2417 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418 if remote_url:
2419 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002420 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002421 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2422 settings.GetPendingRefPrefix())
2423 if target_ref:
2424 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002426 # Look for dependent patchsets. See crbug.com/480453 for more details.
2427 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2428 upstream_branch = ShortBranchName(upstream_branch)
2429 if remote is '.':
2430 # A local branch is being tracked.
2431 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002432 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002433 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002434 print ('Skipping dependency patchset upload because git config '
2435 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002436 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002437 else:
2438 auth_config = auth.extract_auth_config_from_options(options)
2439 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2440 branch_cl_issue_url = branch_cl.GetIssueURL()
2441 branch_cl_issue = branch_cl.GetIssue()
2442 branch_cl_patchset = branch_cl.GetPatchset()
2443 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2444 upload_args.extend(
2445 ['--depends_on_patchset', '%s:%s' % (
2446 branch_cl_issue, branch_cl_patchset)])
2447 print
2448 print ('The current branch (%s) is tracking a local branch (%s) with '
2449 'an associated CL.') % (cl.GetBranch(), local_branch)
2450 print 'Adding %s/#ps%s as a dependency patchset.' % (
2451 branch_cl_issue_url, branch_cl_patchset)
2452 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002453
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002454 project = settings.GetProject()
2455 if project:
2456 upload_args.extend(['--project', project])
2457
rmistry@google.comef966222015-04-07 11:15:01 +00002458 if options.cq_dry_run:
2459 upload_args.extend(['--cq_dry_run'])
2460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002461 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002462 upload_args = ['upload'] + upload_args + args
2463 logging.info('upload.RealMain(%s)', upload_args)
2464 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002465 issue = int(issue)
2466 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002467 except KeyboardInterrupt:
2468 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002469 except:
2470 # If we got an exception after the user typed a description for their
2471 # change, back up the description before re-raising.
2472 if change_desc:
2473 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2474 print '\nGot exception while uploading -- saving description to %s\n' \
2475 % backup_path
2476 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002477 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 backup_file.close()
2479 raise
2480
2481 if not cl.GetIssue():
2482 cl.SetIssue(issue)
2483 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002484
2485 if options.use_commit_queue:
2486 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002487 return 0
2488
2489
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002490def cleanup_list(l):
2491 """Fixes a list so that comma separated items are put as individual items.
2492
2493 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2494 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2495 """
2496 items = sum((i.split(',') for i in l), [])
2497 stripped_items = (i.strip() for i in items)
2498 return sorted(filter(None, stripped_items))
2499
2500
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002501@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002502def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002503 """Uploads the current changelist to codereview.
2504
2505 Can skip dependency patchset uploads for a branch by running:
2506 git config branch.branch_name.skip-deps-uploads True
2507 To unset run:
2508 git config --unset branch.branch_name.skip-deps-uploads
2509 Can also set the above globally by using the --global flag.
2510 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002511 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2512 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002513 parser.add_option('--bypass-watchlists', action='store_true',
2514 dest='bypass_watchlists',
2515 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002516 parser.add_option('-f', action='store_true', dest='force',
2517 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002518 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002519 parser.add_option('-t', dest='title',
2520 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002521 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002522 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002523 help='reviewer email addresses')
2524 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002525 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002526 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002527 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002528 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002529 parser.add_option('--emulate_svn_auto_props',
2530 '--emulate-svn-auto-props',
2531 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002532 dest="emulate_svn_auto_props",
2533 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002534 parser.add_option('-c', '--use-commit-queue', action='store_true',
2535 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002536 parser.add_option('--private', action='store_true',
2537 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002538 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002539 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002540 metavar='TARGET',
2541 help='Apply CL to remote ref TARGET. ' +
2542 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002543 parser.add_option('--squash', action='store_true',
2544 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002545 parser.add_option('--no-squash', action='store_true',
2546 help='Don\'t squash multiple commits into one ' +
2547 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002548 parser.add_option('--email', default=None,
2549 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002550 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2551 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002552 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2553 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002554 help='Send the patchset to do a CQ dry run right after '
2555 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002556 parser.add_option('--dependencies', action='store_true',
2557 help='Uploads CLs of all the local branches that depend on '
2558 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002559
rmistry@google.com2dd99862015-06-22 12:22:18 +00002560 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002561 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002562 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002563 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002564 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002565
sbc@chromium.org71437c02015-04-09 19:29:40 +00002566 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002567 return 1
2568
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002569 options.reviewers = cleanup_list(options.reviewers)
2570 options.cc = cleanup_list(options.cc)
2571
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002572 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002573 if args:
2574 # TODO(ukai): is it ok for gerrit case?
2575 base_branch = args[0]
2576 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002577 if cl.GetBranch() is None:
2578 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2579
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002580 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002581 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002582 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002583
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002584 # Make sure authenticated to Rietveld before running expensive hooks. It is
2585 # a fast, best efforts check. Rietveld still can reject the authentication
2586 # during the actual upload.
2587 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2588 authenticator = auth.get_authenticator_for_host(
2589 cl.GetRietveldServer(), auth_config)
2590 if not authenticator.has_cached_credentials():
2591 raise auth.LoginRequiredError(cl.GetRietveldServer())
2592
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002593 # Apply watchlists on upload.
2594 change = cl.GetChange(base_branch, None)
2595 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2596 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002597 if not options.bypass_watchlists:
2598 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002599
ukai@chromium.orge8077812012-02-03 03:41:46 +00002600 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002601 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002602 # Set the reviewer list now so that presubmit checks can access it.
2603 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002604 change_description.update_reviewers(options.reviewers,
2605 options.tbr_owners,
2606 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002607 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002608 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002609 may_prompt=not options.force,
2610 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002611 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002612 if not hook_results.should_continue():
2613 return 1
2614 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002615 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002616
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002617 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002618 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002619 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002620 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002621 print ('The last upload made from this repository was patchset #%d but '
2622 'the most recent patchset on the server is #%d.'
2623 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002624 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2625 'from another machine or branch the patch you\'re uploading now '
2626 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002627 ask_for_data('About to upload; enter to confirm.')
2628
iannucci@chromium.org79540052012-10-19 23:15:26 +00002629 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002630 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002631 if options.squash and options.no_squash:
2632 DieWithError('Can only use one of --squash or --no-squash')
2633
2634 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2635 not options.no_squash)
2636
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002637 ret = GerritUpload(options, args, cl, change)
2638 else:
2639 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002640 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002641 git_set_branch_value('last-upload-hash',
2642 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002643 # Run post upload hooks, if specified.
2644 if settings.GetRunPostUploadHook():
2645 presubmit_support.DoPostUploadExecuter(
2646 change,
2647 cl,
2648 settings.GetRoot(),
2649 options.verbose,
2650 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002651
rmistry@google.com2dd99862015-06-22 12:22:18 +00002652 # Upload all dependencies if specified.
2653 if options.dependencies:
2654 print
2655 print '--dependencies has been specified.'
2656 print 'All dependent local branches will be re-uploaded.'
2657 print
2658 # Remove the dependencies flag from args so that we do not end up in a
2659 # loop.
2660 orig_args.remove('--dependencies')
2661 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002662 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002663
2664
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002665def IsSubmoduleMergeCommit(ref):
2666 # When submodules are added to the repo, we expect there to be a single
2667 # non-git-svn merge commit at remote HEAD with a signature comment.
2668 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002669 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002670 return RunGit(cmd) != ''
2671
2672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002673def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002674 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002675
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002676 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002677 Updates changelog with metadata (e.g. pointer to review).
2678 Pushes/dcommits the code upstream.
2679 Updates review and closes.
2680 """
2681 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2682 help='bypass upload presubmit hook')
2683 parser.add_option('-m', dest='message',
2684 help="override review description")
2685 parser.add_option('-f', action='store_true', dest='force',
2686 help="force yes to questions (don't prompt)")
2687 parser.add_option('-c', dest='contributor',
2688 help="external contributor for patch (appended to " +
2689 "description and used as author for git). Should be " +
2690 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002691 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002692 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002694 auth_config = auth.extract_auth_config_from_options(options)
2695
2696 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002698 current = cl.GetBranch()
2699 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2700 if not settings.GetIsGitSvn() and remote == '.':
2701 print
2702 print 'Attempting to push branch %r into another local branch!' % current
2703 print
2704 print 'Either reparent this branch on top of origin/master:'
2705 print ' git reparent-branch --root'
2706 print
2707 print 'OR run `git rebase-update` if you think the parent branch is already'
2708 print 'committed.'
2709 print
2710 print ' Current parent: %r' % upstream_branch
2711 return 1
2712
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002713 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714 # Default to merging against our best guess of the upstream branch.
2715 args = [cl.GetUpstreamBranch()]
2716
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002717 if options.contributor:
2718 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2719 print "Please provide contibutor as 'First Last <email@example.com>'"
2720 return 1
2721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002722 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002723 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724
sbc@chromium.org71437c02015-04-09 19:29:40 +00002725 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726 return 1
2727
2728 # This rev-list syntax means "show all commits not in my branch that
2729 # are in base_branch".
2730 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2731 base_branch]).splitlines()
2732 if upstream_commits:
2733 print ('Base branch "%s" has %d commits '
2734 'not in this branch.' % (base_branch, len(upstream_commits)))
2735 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2736 return 1
2737
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002738 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002739 svn_head = None
2740 if cmd == 'dcommit' or base_has_submodules:
2741 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2742 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002745 # If the base_head is a submodule merge commit, the first parent of the
2746 # base_head should be a git-svn commit, which is what we're interested in.
2747 base_svn_head = base_branch
2748 if base_has_submodules:
2749 base_svn_head += '^1'
2750
2751 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 if extra_commits:
2753 print ('This branch has %d additional commits not upstreamed yet.'
2754 % len(extra_commits.splitlines()))
2755 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2756 'before attempting to %s.' % (base_branch, cmd))
2757 return 1
2758
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002759 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002760 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002761 author = None
2762 if options.contributor:
2763 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002764 hook_results = cl.RunHook(
2765 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002766 may_prompt=not options.force,
2767 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002768 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002769 if not hook_results.should_continue():
2770 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002772 # Check the tree status if the tree status URL is set.
2773 status = GetTreeStatus()
2774 if 'closed' == status:
2775 print('The tree is closed. Please wait for it to reopen. Use '
2776 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2777 return 1
2778 elif 'unknown' == status:
2779 print('Unable to determine tree status. Please verify manually and '
2780 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2781 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002783 change_desc = ChangeDescription(options.message)
2784 if not change_desc.description and cl.GetIssue():
2785 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002786
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002787 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002788 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002789 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002790 else:
2791 print 'No description set.'
2792 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2793 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002794
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002795 # Keep a separate copy for the commit message, because the commit message
2796 # contains the link to the Rietveld issue, while the Rietveld message contains
2797 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002798 # Keep a separate copy for the commit message.
2799 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002800 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002801
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002802 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002803 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002804 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002805 # after it. Add a period on a new line to circumvent this. Also add a space
2806 # before the period to make sure that Gitiles continues to correctly resolve
2807 # the URL.
2808 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002810 commit_desc.append_footer('Patch from %s.' % options.contributor)
2811
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002812 print('Description:')
2813 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002814
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002815 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002817 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002818
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002819 # We want to squash all this branch's commits into one commit with the proper
2820 # description. We do this by doing a "reset --soft" to the base branch (which
2821 # keeps the working copy the same), then dcommitting that. If origin/master
2822 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2823 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002824 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002825 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2826 # Delete the branches if they exist.
2827 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2828 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2829 result = RunGitWithCode(showref_cmd)
2830 if result[0] == 0:
2831 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832
2833 # We might be in a directory that's present in this branch but not in the
2834 # trunk. Move up to the top of the tree so that git commands that expect a
2835 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002836 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002837 if rel_base_path:
2838 os.chdir(rel_base_path)
2839
2840 # Stuff our change into the merge branch.
2841 # We wrap in a try...finally block so if anything goes wrong,
2842 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002843 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002844 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002845 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002846 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002848 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002849 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002850 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002851 RunGit(
2852 [
2853 'commit', '--author', options.contributor,
2854 '-m', commit_desc.description,
2855 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002856 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002857 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002858 if base_has_submodules:
2859 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2860 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2861 RunGit(['checkout', CHERRY_PICK_BRANCH])
2862 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002863 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002864 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002865 pending_prefix = settings.GetPendingRefPrefix()
2866 if not pending_prefix or branch.startswith(pending_prefix):
2867 # If not using refs/pending/heads/* at all, or target ref is already set
2868 # to pending, then push to the target ref directly.
2869 retcode, output = RunGitWithCode(
2870 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002871 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002872 else:
2873 # Cherry-pick the change on top of pending ref and then push it.
2874 assert branch.startswith('refs/'), branch
2875 assert pending_prefix[-1] == '/', pending_prefix
2876 pending_ref = pending_prefix + branch[len('refs/'):]
2877 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002878 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002879 if retcode == 0:
2880 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881 else:
2882 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002883 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002884 'svn', 'dcommit',
2885 '-C%s' % options.similarity,
2886 '--no-rebase', '--rmdir',
2887 ]
2888 if settings.GetForceHttpsCommitUrl():
2889 # Allow forcing https commit URLs for some projects that don't allow
2890 # committing to http URLs (like Google Code).
2891 remote_url = cl.GetGitSvnRemoteUrl()
2892 if urlparse.urlparse(remote_url).scheme == 'http':
2893 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002894 cmd_args.append('--commit-url=%s' % remote_url)
2895 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002896 if 'Committed r' in output:
2897 revision = re.match(
2898 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2899 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002900 finally:
2901 # And then swap back to the original branch and clean up.
2902 RunGit(['checkout', '-q', cl.GetBranch()])
2903 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002904 if base_has_submodules:
2905 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002906
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002907 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002908 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002909 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002910
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002911 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002912 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002913 try:
2914 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2915 # We set pushed_to_pending to False, since it made it all the way to the
2916 # real ref.
2917 pushed_to_pending = False
2918 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002919 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002922 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002924 if not to_pending:
2925 if viewvc_url and revision:
2926 change_desc.append_footer(
2927 'Committed: %s%s' % (viewvc_url, revision))
2928 elif revision:
2929 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930 print ('Closing issue '
2931 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002932 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002933 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002934 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002935 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002936 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002937 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002938 if options.bypass_hooks:
2939 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2940 else:
2941 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002942 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002943 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002944
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002945 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002946 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2947 print 'The commit is in the pending queue (%s).' % pending_ref
2948 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002949 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002950 'footer.' % branch)
2951
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002952 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2953 if os.path.isfile(hook):
2954 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002955
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002956 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957
2958
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002959def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2960 print
2961 print 'Waiting for commit to be landed on %s...' % real_ref
2962 print '(If you are impatient, you may Ctrl-C once without harm)'
2963 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2964 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2965
2966 loop = 0
2967 while True:
2968 sys.stdout.write('fetching (%d)... \r' % loop)
2969 sys.stdout.flush()
2970 loop += 1
2971
2972 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2973 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2974 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2975 for commit in commits.splitlines():
2976 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2977 print 'Found commit on %s' % real_ref
2978 return commit
2979
2980 current_rev = to_rev
2981
2982
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002983def PushToGitPending(remote, pending_ref, upstream_ref):
2984 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2985
2986 Returns:
2987 (retcode of last operation, output log of last operation).
2988 """
2989 assert pending_ref.startswith('refs/'), pending_ref
2990 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2991 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2992 code = 0
2993 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002994 max_attempts = 3
2995 attempts_left = max_attempts
2996 while attempts_left:
2997 if attempts_left != max_attempts:
2998 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2999 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003000
3001 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003002 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003003 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003004 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003005 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003006 print 'Fetch failed with exit code %d.' % code
3007 if out.strip():
3008 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003009 continue
3010
3011 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003012 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003013 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003014 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003015 if code:
3016 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003017 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3018 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003019 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3020 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003021 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003022 return code, out
3023
3024 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003025 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003026 code, out = RunGitWithCode(
3027 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3028 if code == 0:
3029 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003030 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003031 return code, out
3032
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003033 print 'Push failed with exit code %d.' % code
3034 if out.strip():
3035 print out.strip()
3036 if IsFatalPushFailure(out):
3037 print (
3038 'Fatal push error. Make sure your .netrc credentials and git '
3039 'user.email are correct and you have push access to the repo.')
3040 return code, out
3041
3042 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003043 return code, out
3044
3045
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003046def IsFatalPushFailure(push_stdout):
3047 """True if retrying push won't help."""
3048 return '(prohibited by Gerrit)' in push_stdout
3049
3050
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003051@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003052def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003053 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003054 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003055 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003056 # If it looks like previous commits were mirrored with git-svn.
3057 message = """This repository appears to be a git-svn mirror, but no
3058upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3059 else:
3060 message = """This doesn't appear to be an SVN repository.
3061If your project has a true, writeable git repository, you probably want to run
3062'git cl land' instead.
3063If your project has a git mirror of an upstream SVN master, you probably need
3064to run 'git svn init'.
3065
3066Using the wrong command might cause your commit to appear to succeed, and the
3067review to be closed, without actually landing upstream. If you choose to
3068proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003069 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003070 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003071 return SendUpstream(parser, args, 'dcommit')
3072
3073
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003074@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003075def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003076 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003077 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078 print('This appears to be an SVN repository.')
3079 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003080 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003081 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003082 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003083
3084
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003085def ParseIssueNum(arg):
3086 """Parses the issue number from args if present otherwise returns None."""
3087 if re.match(r'\d+', arg):
3088 return arg
3089 if arg.startswith('http'):
3090 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3091 return None
3092
3093
3094@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003095def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003096 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097 parser.add_option('-b', dest='newbranch',
3098 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003099 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003100 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003101 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3102 help='Change to the directory DIR immediately, '
3103 'before doing anything else.')
3104 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003105 help='failed patches spew .rej files rather than '
3106 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003107 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3108 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003109
3110 group = optparse.OptionGroup(parser,
3111 """Options for continuing work on the current issue uploaded
3112from a different clone (e.g. different machine). Must be used independently from
3113the other options. No issue number should be specified, and the branch must have
3114an issue number associated with it""")
3115 group.add_option('--reapply', action='store_true',
3116 dest='reapply',
3117 help="""Reset the branch and reapply the issue.
3118CAUTION: This will undo any local changes in this branch""")
3119
3120 group.add_option('--pull', action='store_true', dest='pull',
3121 help="Performs a pull before reapplying.")
3122 parser.add_option_group(group)
3123
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003124 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003125 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003126 auth_config = auth.extract_auth_config_from_options(options)
3127
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003128 issue_arg = None
3129 if options.reapply :
3130 if len(args) > 0:
3131 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003132
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003133 cl = Changelist()
3134 issue_arg = cl.GetIssue()
3135 upstream = cl.GetUpstreamBranch()
3136 if upstream == None:
3137 parser.error("No upstream branch specified. Cannot reset branch")
3138
3139 RunGit(['reset', '--hard', upstream])
3140 if options.pull:
3141 RunGit(['pull'])
3142 else:
3143 if len(args) != 1:
3144 parser.error("Must specify issue number")
3145
3146 issue_arg = ParseIssueNum(args[0])
3147
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003148 # The patch URL works because ParseIssueNum won't do any substitution
3149 # as the re.sub pattern fails to match and just returns it.
3150 if issue_arg == None:
3151 parser.print_help()
3152 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003153
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003154 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003155 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003156 return 1
3157
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003158 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003159 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003160
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003161 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003162 if options.reapply:
3163 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003164 if options.force:
3165 RunGit(['branch', '-D', options.newbranch],
3166 stderr=subprocess2.PIPE, error_ok=True)
3167 RunGit(['checkout', '-b', options.newbranch,
3168 Changelist().GetUpstreamBranch()])
3169
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003170 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003171 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003172
3173
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003174def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003175 # PatchIssue should never be called with a dirty tree. It is up to the
3176 # caller to check this, but just in case we assert here since the
3177 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003178 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003179
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003180 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003182 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003183 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003184 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003185 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003187 # Assume it's a URL to the patch. Default to https.
3188 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003189 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003190 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 DieWithError('Must pass an issue ID or full URL for '
3192 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003193 issue = int(match.group(2))
3194 cl = Changelist(issue=issue, auth_config=auth_config)
3195 cl.rietveld_server = match.group(1)
3196 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003197 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003199 # Switch up to the top-level directory, if necessary, in preparation for
3200 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003201 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003202 if top:
3203 os.chdir(top)
3204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 # Git patches have a/ at the beginning of source paths. We strip that out
3206 # with a sed script rather than the -p flag to patch so we can feed either
3207 # Git or svn-style patches into the same apply command.
3208 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003209 try:
3210 patch_data = subprocess2.check_output(
3211 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3212 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 DieWithError('Git patch mungling failed.')
3214 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003215
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003216 # We use "git apply" to apply the patch instead of "patch" so that we can
3217 # pick up file adds.
3218 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003219 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003220 if directory:
3221 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003222 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003224 elif IsGitVersionAtLeast('1.7.12'):
3225 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003226 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003227 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003228 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003229 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003230 print 'Failed to apply the patch'
3231 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232
3233 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003234 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003235 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3236 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003237 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3238 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003239 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003241 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003242 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243 else:
3244 print "Patch applied to index."
3245 return 0
3246
3247
3248def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003249 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 # Provide a wrapper for git svn rebase to help avoid accidental
3251 # git svn dcommit.
3252 # It's the only command that doesn't use parser at all since we just defer
3253 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003254
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003255 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003256
3257
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003258def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 """Fetches the tree status and returns either 'open', 'closed',
3260 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003261 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262 if url:
3263 status = urllib2.urlopen(url).read().lower()
3264 if status.find('closed') != -1 or status == '0':
3265 return 'closed'
3266 elif status.find('open') != -1 or status == '1':
3267 return 'open'
3268 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269 return 'unset'
3270
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272def GetTreeStatusReason():
3273 """Fetches the tree status from a json url and returns the message
3274 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003275 url = settings.GetTreeStatusUrl()
3276 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003277 connection = urllib2.urlopen(json_url)
3278 status = json.loads(connection.read())
3279 connection.close()
3280 return status['message']
3281
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003282
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003283def GetBuilderMaster(bot_list):
3284 """For a given builder, fetch the master from AE if available."""
3285 map_url = 'https://builders-map.appspot.com/'
3286 try:
3287 master_map = json.load(urllib2.urlopen(map_url))
3288 except urllib2.URLError as e:
3289 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3290 (map_url, e))
3291 except ValueError as e:
3292 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3293 if not master_map:
3294 return None, 'Failed to build master map.'
3295
3296 result_master = ''
3297 for bot in bot_list:
3298 builder = bot.split(':', 1)[0]
3299 master_list = master_map.get(builder, [])
3300 if not master_list:
3301 return None, ('No matching master for builder %s.' % builder)
3302 elif len(master_list) > 1:
3303 return None, ('The builder name %s exists in multiple masters %s.' %
3304 (builder, master_list))
3305 else:
3306 cur_master = master_list[0]
3307 if not result_master:
3308 result_master = cur_master
3309 elif result_master != cur_master:
3310 return None, 'The builders do not belong to the same master.'
3311 return result_master, None
3312
3313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003314def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003315 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003316 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317 status = GetTreeStatus()
3318 if 'unset' == status:
3319 print 'You must configure your tree status URL by running "git cl config".'
3320 return 2
3321
3322 print "The tree is %s" % status
3323 print
3324 print GetTreeStatusReason()
3325 if status != 'open':
3326 return 1
3327 return 0
3328
3329
maruel@chromium.org15192402012-09-06 12:38:29 +00003330def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003331 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003332 group = optparse.OptionGroup(parser, "Try job options")
3333 group.add_option(
3334 "-b", "--bot", action="append",
3335 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3336 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003337 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003338 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003339 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003340 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003341 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003342 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003343 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003344 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003345 "-r", "--revision",
3346 help="Revision to use for the try job; default: the "
3347 "revision will be determined by the try server; see "
3348 "its waterfall for more info")
3349 group.add_option(
3350 "-c", "--clobber", action="store_true", default=False,
3351 help="Force a clobber before building; e.g. don't do an "
3352 "incremental build")
3353 group.add_option(
3354 "--project",
3355 help="Override which project to use. Projects are defined "
3356 "server-side to define what default bot set to use")
3357 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003358 "-p", "--property", dest="properties", action="append", default=[],
3359 help="Specify generic properties in the form -p key1=value1 -p "
3360 "key2=value2 etc (buildbucket only). The value will be treated as "
3361 "json if decodable, or as string otherwise.")
3362 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003363 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003364 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003365 "--use-rietveld", action="store_true", default=False,
3366 help="Use Rietveld to trigger try jobs.")
3367 group.add_option(
3368 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3369 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003370 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003371 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003372 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003373 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003374
machenbach@chromium.org45453142015-09-15 08:45:22 +00003375 if options.use_rietveld and options.properties:
3376 parser.error('Properties can only be specified with buildbucket')
3377
3378 # Make sure that all properties are prop=value pairs.
3379 bad_params = [x for x in options.properties if '=' not in x]
3380 if bad_params:
3381 parser.error('Got properties with missing "=": %s' % bad_params)
3382
maruel@chromium.org15192402012-09-06 12:38:29 +00003383 if args:
3384 parser.error('Unknown arguments: %s' % args)
3385
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003386 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003387 if not cl.GetIssue():
3388 parser.error('Need to upload first')
3389
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003390 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003391 if props.get('closed'):
3392 parser.error('Cannot send tryjobs for a closed CL')
3393
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003394 if props.get('private'):
3395 parser.error('Cannot use trybots with private issue')
3396
maruel@chromium.org15192402012-09-06 12:38:29 +00003397 if not options.name:
3398 options.name = cl.GetBranch()
3399
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003400 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003401 options.master, err_msg = GetBuilderMaster(options.bot)
3402 if err_msg:
3403 parser.error('Tryserver master cannot be found because: %s\n'
3404 'Please manually specify the tryserver master'
3405 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003406
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003407 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003408 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003409 if not options.bot:
3410 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003411
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003412 # Get try masters from PRESUBMIT.py files.
3413 masters = presubmit_support.DoGetTryMasters(
3414 change,
3415 change.LocalPaths(),
3416 settings.GetRoot(),
3417 None,
3418 None,
3419 options.verbose,
3420 sys.stdout)
3421 if masters:
3422 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003423
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003424 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3425 options.bot = presubmit_support.DoGetTrySlaves(
3426 change,
3427 change.LocalPaths(),
3428 settings.GetRoot(),
3429 None,
3430 None,
3431 options.verbose,
3432 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003433
3434 if not options.bot:
3435 # Get try masters from cq.cfg if any.
3436 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3437 # location.
3438 cq_cfg = os.path.join(change.RepositoryRoot(),
3439 'infra', 'config', 'cq.cfg')
3440 if os.path.exists(cq_cfg):
3441 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003442 cq_masters = commit_queue.get_master_builder_map(
3443 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003444 for master, builders in cq_masters.iteritems():
3445 for builder in builders:
3446 # Skip presubmit builders, because these will fail without LGTM.
3447 if 'presubmit' not in builder.lower():
3448 masters.setdefault(master, {})[builder] = ['defaulttests']
3449 if masters:
3450 return masters
3451
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003452 if not options.bot:
3453 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003454
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003455 builders_and_tests = {}
3456 # TODO(machenbach): The old style command-line options don't support
3457 # multiple try masters yet.
3458 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3459 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3460
3461 for bot in old_style:
3462 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003463 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003464 elif ',' in bot:
3465 parser.error('Specify one bot per --bot flag')
3466 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003467 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003468
3469 for bot, tests in new_style:
3470 builders_and_tests.setdefault(bot, []).extend(tests)
3471
3472 # Return a master map with one master to be backwards compatible. The
3473 # master name defaults to an empty string, which will cause the master
3474 # not to be set on rietveld (deprecated).
3475 return {options.master: builders_and_tests}
3476
3477 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003478
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003479 for builders in masters.itervalues():
3480 if any('triggered' in b for b in builders):
3481 print >> sys.stderr, (
3482 'ERROR You are trying to send a job to a triggered bot. This type of'
3483 ' bot requires an\ninitial job from a parent (usually a builder). '
3484 'Instead send your job to the parent.\n'
3485 'Bot list: %s' % builders)
3486 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003487
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003488 patchset = cl.GetMostRecentPatchset()
3489 if patchset and patchset != cl.GetPatchset():
3490 print(
3491 '\nWARNING Mismatch between local config and server. Did a previous '
3492 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3493 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003494 if options.luci:
3495 trigger_luci_job(cl, masters, options)
3496 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003497 try:
3498 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3499 except BuildbucketResponseException as ex:
3500 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003501 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003502 except Exception as e:
3503 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3504 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3505 e, stacktrace)
3506 return 1
3507 else:
3508 try:
3509 cl.RpcServer().trigger_distributed_try_jobs(
3510 cl.GetIssue(), patchset, options.name, options.clobber,
3511 options.revision, masters)
3512 except urllib2.HTTPError as e:
3513 if e.code == 404:
3514 print('404 from rietveld; '
3515 'did you mean to use "git try" instead of "git cl try"?')
3516 return 1
3517 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003518
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003519 for (master, builders) in sorted(masters.iteritems()):
3520 if master:
3521 print 'Master: %s' % master
3522 length = max(len(builder) for builder in builders)
3523 for builder in sorted(builders):
3524 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003525 return 0
3526
3527
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003528def CMDtry_results(parser, args):
3529 group = optparse.OptionGroup(parser, "Try job results options")
3530 group.add_option(
3531 "-p", "--patchset", type=int, help="patchset number if not current.")
3532 group.add_option(
3533 "--print-master", action='store_true', help="print master name as well")
3534 group.add_option(
3535 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3536 help="Host of buildbucket. The default host is %default.")
3537 parser.add_option_group(group)
3538 auth.add_auth_options(parser)
3539 options, args = parser.parse_args(args)
3540 if args:
3541 parser.error('Unrecognized args: %s' % ' '.join(args))
3542
3543 auth_config = auth.extract_auth_config_from_options(options)
3544 cl = Changelist(auth_config=auth_config)
3545 if not cl.GetIssue():
3546 parser.error('Need to upload first')
3547
3548 if not options.patchset:
3549 options.patchset = cl.GetMostRecentPatchset()
3550 if options.patchset and options.patchset != cl.GetPatchset():
3551 print(
3552 '\nWARNING Mismatch between local config and server. Did a previous '
3553 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3554 'Continuing using\npatchset %s.\n' % options.patchset)
3555 try:
3556 jobs = fetch_try_jobs(auth_config, cl, options)
3557 except BuildbucketResponseException as ex:
3558 print 'Buildbucket error: %s' % ex
3559 return 1
3560 except Exception as e:
3561 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3562 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3563 e, stacktrace)
3564 return 1
3565 print_tryjobs(options, jobs)
3566 return 0
3567
3568
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003569@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003571 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003572 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003573 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003574 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003576 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003577 if args:
3578 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003579 branch = cl.GetBranch()
3580 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003581 cl = Changelist()
3582 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003583
3584 # Clear configured merge-base, if there is one.
3585 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003586 else:
3587 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588 return 0
3589
3590
thestig@chromium.org00858c82013-12-02 23:08:03 +00003591def CMDweb(parser, args):
3592 """Opens the current CL in the web browser."""
3593 _, args = parser.parse_args(args)
3594 if args:
3595 parser.error('Unrecognized args: %s' % ' '.join(args))
3596
3597 issue_url = Changelist().GetIssueURL()
3598 if not issue_url:
3599 print >> sys.stderr, 'ERROR No issue to open'
3600 return 1
3601
3602 webbrowser.open(issue_url)
3603 return 0
3604
3605
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003606def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003607 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003608 auth.add_auth_options(parser)
3609 options, args = parser.parse_args(args)
3610 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003611 if args:
3612 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003613 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003614 props = cl.GetIssueProperties()
3615 if props.get('private'):
3616 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003617 cl.SetFlag('commit', '1')
3618 return 0
3619
3620
groby@chromium.org411034a2013-02-26 15:12:01 +00003621def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003622 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003623 auth.add_auth_options(parser)
3624 options, args = parser.parse_args(args)
3625 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003626 if args:
3627 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003628 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003629 # Ensure there actually is an issue to close.
3630 cl.GetDescription()
3631 cl.CloseIssue()
3632 return 0
3633
3634
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003635def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003636 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003637 auth.add_auth_options(parser)
3638 options, args = parser.parse_args(args)
3639 auth_config = auth.extract_auth_config_from_options(options)
3640 if args:
3641 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003642
3643 # Uncommitted (staged and unstaged) changes will be destroyed by
3644 # "git reset --hard" if there are merging conflicts in PatchIssue().
3645 # Staged changes would be committed along with the patch from last
3646 # upload, hence counted toward the "last upload" side in the final
3647 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003648 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003649 return 1
3650
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003652 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003653 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003654 if not issue:
3655 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003656 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003657 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003658
3659 # Create a new branch based on the merge-base
3660 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3661 try:
3662 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003663 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003664 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003665 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003666 return rtn
3667
wychen@chromium.org06928532015-02-03 02:11:29 +00003668 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003669 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003670 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003671 finally:
3672 RunGit(['checkout', '-q', branch])
3673 RunGit(['branch', '-D', TMP_BRANCH])
3674
3675 return 0
3676
3677
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003678def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003679 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003680 parser.add_option(
3681 '--no-color',
3682 action='store_true',
3683 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003684 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003685 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003686 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003687
3688 author = RunGit(['config', 'user.email']).strip() or None
3689
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003690 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003691
3692 if args:
3693 if len(args) > 1:
3694 parser.error('Unknown args')
3695 base_branch = args[0]
3696 else:
3697 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003698 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003699
3700 change = cl.GetChange(base_branch, None)
3701 return owners_finder.OwnersFinder(
3702 [f.LocalPath() for f in
3703 cl.GetChange(base_branch, None).AffectedFiles()],
3704 change.RepositoryRoot(), author,
3705 fopen=file, os_path=os.path, glob=glob.glob,
3706 disable_color=options.no_color).run()
3707
3708
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003709def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003710 """Generates a diff command."""
3711 # Generate diff for the current branch's changes.
3712 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3713 upstream_commit, '--' ]
3714
3715 if args:
3716 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003717 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003718 diff_cmd.append(arg)
3719 else:
3720 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003721
3722 return diff_cmd
3723
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003724def MatchingFileType(file_name, extensions):
3725 """Returns true if the file name ends with one of the given extensions."""
3726 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003727
enne@chromium.org555cfe42014-01-29 18:21:39 +00003728@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003729def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003730 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003731 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003732 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003733 parser.add_option('--full', action='store_true',
3734 help='Reformat the full content of all touched files')
3735 parser.add_option('--dry-run', action='store_true',
3736 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003737 parser.add_option('--python', action='store_true',
3738 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003739 parser.add_option('--diff', action='store_true',
3740 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003741 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003742
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003743 # git diff generates paths against the root of the repository. Change
3744 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003745 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003746 if rel_base_path:
3747 os.chdir(rel_base_path)
3748
digit@chromium.org29e47272013-05-17 17:01:46 +00003749 # Grab the merge-base commit, i.e. the upstream commit of the current
3750 # branch when it was created or the last time it was rebased. This is
3751 # to cover the case where the user may have called "git fetch origin",
3752 # moving the origin branch to a newer commit, but hasn't rebased yet.
3753 upstream_commit = None
3754 cl = Changelist()
3755 upstream_branch = cl.GetUpstreamBranch()
3756 if upstream_branch:
3757 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3758 upstream_commit = upstream_commit.strip()
3759
3760 if not upstream_commit:
3761 DieWithError('Could not find base commit for this branch. '
3762 'Are you in detached state?')
3763
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003764 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3765 diff_output = RunGit(changed_files_cmd)
3766 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003767 # Filter out files deleted by this CL
3768 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003769
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003770 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3771 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3772 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003773 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003774
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003775 top_dir = os.path.normpath(
3776 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3777
3778 # Locate the clang-format binary in the checkout
3779 try:
3780 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3781 except clang_format.NotFoundError, e:
3782 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003783
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003784 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3785 # formatted. This is used to block during the presubmit.
3786 return_value = 0
3787
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003788 if clang_diff_files:
3789 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003790 cmd = [clang_format_tool]
3791 if not opts.dry_run and not opts.diff:
3792 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003793 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003794 if opts.diff:
3795 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003796 else:
3797 env = os.environ.copy()
3798 env['PATH'] = str(os.path.dirname(clang_format_tool))
3799 try:
3800 script = clang_format.FindClangFormatScriptInChromiumTree(
3801 'clang-format-diff.py')
3802 except clang_format.NotFoundError, e:
3803 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003804
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003805 cmd = [sys.executable, script, '-p0']
3806 if not opts.dry_run and not opts.diff:
3807 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003808
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003809 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3810 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003811
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003812 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3813 if opts.diff:
3814 sys.stdout.write(stdout)
3815 if opts.dry_run and len(stdout) > 0:
3816 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003817
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003818 # Similar code to above, but using yapf on .py files rather than clang-format
3819 # on C/C++ files
3820 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003821 yapf_tool = gclient_utils.FindExecutable('yapf')
3822 if yapf_tool is None:
3823 DieWithError('yapf not found in PATH')
3824
3825 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003826 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003827 cmd = [yapf_tool]
3828 if not opts.dry_run and not opts.diff:
3829 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003830 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003831 if opts.diff:
3832 sys.stdout.write(stdout)
3833 else:
3834 # TODO(sbc): yapf --lines mode still has some issues.
3835 # https://github.com/google/yapf/issues/154
3836 DieWithError('--python currently only works with --full')
3837
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003838 # Dart's formatter does not have the nice property of only operating on
3839 # modified chunks, so hard code full.
3840 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003841 try:
3842 command = [dart_format.FindDartFmtToolInChromiumTree()]
3843 if not opts.dry_run and not opts.diff:
3844 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003845 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003846
ppi@chromium.org6593d932016-03-03 15:41:15 +00003847 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003848 if opts.dry_run and stdout:
3849 return_value = 2
3850 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003851 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3852 'found in this checkout. Files in other languages are still ' +
3853 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003854
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003855 # Format GN build files. Always run on full build files for canonical form.
3856 if gn_diff_files:
3857 cmd = ['gn', 'format']
3858 if not opts.dry_run and not opts.diff:
3859 cmd.append('--in-place')
3860 for gn_diff_file in gn_diff_files:
3861 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3862 if opts.diff:
3863 sys.stdout.write(stdout)
3864
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003865 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003866
3867
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003868@subcommand.usage('<codereview url or issue id>')
3869def CMDcheckout(parser, args):
3870 """Checks out a branch associated with a given Rietveld issue."""
3871 _, args = parser.parse_args(args)
3872
3873 if len(args) != 1:
3874 parser.print_help()
3875 return 1
3876
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003877 target_issue = ParseIssueNum(args[0])
3878 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003879 parser.print_help()
3880 return 1
3881
3882 key_and_issues = [x.split() for x in RunGit(
3883 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3884 .splitlines()]
3885 branches = []
3886 for key, issue in key_and_issues:
3887 if issue == target_issue:
3888 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3889
3890 if len(branches) == 0:
3891 print 'No branch found for issue %s.' % target_issue
3892 return 1
3893 if len(branches) == 1:
3894 RunGit(['checkout', branches[0]])
3895 else:
3896 print 'Multiple branches match issue %s:' % target_issue
3897 for i in range(len(branches)):
3898 print '%d: %s' % (i, branches[i])
3899 which = raw_input('Choose by index: ')
3900 try:
3901 RunGit(['checkout', branches[int(which)]])
3902 except (IndexError, ValueError):
3903 print 'Invalid selection, not checking out any branch.'
3904 return 1
3905
3906 return 0
3907
3908
maruel@chromium.org29404b52014-09-08 22:58:00 +00003909def CMDlol(parser, args):
3910 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003911 print zlib.decompress(base64.b64decode(
3912 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3913 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3914 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3915 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003916 return 0
3917
3918
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003919class OptionParser(optparse.OptionParser):
3920 """Creates the option parse and add --verbose support."""
3921 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003922 optparse.OptionParser.__init__(
3923 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003924 self.add_option(
3925 '-v', '--verbose', action='count', default=0,
3926 help='Use 2 times for more debugging info')
3927
3928 def parse_args(self, args=None, values=None):
3929 options, args = optparse.OptionParser.parse_args(self, args, values)
3930 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3931 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3932 return options, args
3933
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003934
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003935def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003936 if sys.hexversion < 0x02060000:
3937 print >> sys.stderr, (
3938 '\nYour python version %s is unsupported, please upgrade.\n' %
3939 sys.version.split(' ', 1)[0])
3940 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003941
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003942 # Reload settings.
3943 global settings
3944 settings = Settings()
3945
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003946 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003947 dispatcher = subcommand.CommandDispatcher(__name__)
3948 try:
3949 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003950 except auth.AuthenticationError as e:
3951 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003952 except urllib2.HTTPError, e:
3953 if e.code != 500:
3954 raise
3955 DieWithError(
3956 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3957 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003958 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959
3960
3961if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003962 # These affect sys.stdout so do it outside of main() to simplify mocks in
3963 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003964 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003965 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003966 try:
3967 sys.exit(main(sys.argv[1:]))
3968 except KeyboardInterrupt:
3969 sys.stderr.write('interrupted\n')
3970 sys.exit(1)