blob: f1a450973db18f773e76f21c55c722b5edcb0a63 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000099 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000116 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000122 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000123 stdout=subprocess2.PIPE,
124 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
133 """Returns stdout, suppresses stderr and ingores the return code."""
134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000339 if tests:
340 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000341 if properties:
342 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343 if options.clobber:
344 parameters['properties']['clobber'] = True
345 batch_req_body['builds'].append(
346 {
347 'bucket': bucket,
348 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 'tags': ['builder:%s' % builder,
351 'buildset:%s' % buildset,
352 'master:%s' % master,
353 'user_agent:git_cl_try']
354 }
355 )
356
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 _buildbucket_retry(
358 'triggering tryjobs',
359 http,
360 buildbucket_put_url,
361 'PUT',
362 body=json.dumps(batch_req_body),
363 headers={'Content-Type': 'application/json'}
364 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000365 print_text.append('To see results here, run: git cl try-results')
366 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000368
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def fetch_try_jobs(auth_config, changelist, options):
371 """Fetches tryjobs from buildbucket.
372
373 Returns a map from build id to build info as json dictionary.
374 """
375 rietveld_url = settings.GetDefaultServerUrl()
376 rietveld_host = urlparse.urlparse(rietveld_url).hostname
377 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
378 if authenticator.has_cached_credentials():
379 http = authenticator.authorize(httplib2.Http())
380 else:
381 print ('Warning: Some results might be missing because %s' %
382 # Get the message on how to login.
383 auth.LoginRequiredError(rietveld_host).message)
384 http = httplib2.Http()
385
386 http.force_exception_to_status_code = True
387
388 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
389 hostname=rietveld_host,
390 issue=changelist.GetIssue(),
391 patch=options.patchset)
392 params = {'tag': 'buildset:%s' % buildset}
393
394 builds = {}
395 while True:
396 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
397 hostname=options.buildbucket_host,
398 params=urllib.urlencode(params))
399 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
400 for build in content.get('builds', []):
401 builds[build['id']] = build
402 if 'next_cursor' in content:
403 params['start_cursor'] = content['next_cursor']
404 else:
405 break
406 return builds
407
408
409def print_tryjobs(options, builds):
410 """Prints nicely result of fetch_try_jobs."""
411 if not builds:
412 print 'No tryjobs scheduled'
413 return
414
415 # Make a copy, because we'll be modifying builds dictionary.
416 builds = builds.copy()
417 builder_names_cache = {}
418
419 def get_builder(b):
420 try:
421 return builder_names_cache[b['id']]
422 except KeyError:
423 try:
424 parameters = json.loads(b['parameters_json'])
425 name = parameters['builder_name']
426 except (ValueError, KeyError) as error:
427 print 'WARNING: failed to get builder name for build %s: %s' % (
428 b['id'], error)
429 name = None
430 builder_names_cache[b['id']] = name
431 return name
432
433 def get_bucket(b):
434 bucket = b['bucket']
435 if bucket.startswith('master.'):
436 return bucket[len('master.'):]
437 return bucket
438
439 if options.print_master:
440 name_fmt = '%%-%ds %%-%ds' % (
441 max(len(str(get_bucket(b))) for b in builds.itervalues()),
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % (get_bucket(b), get_builder(b))
445 else:
446 name_fmt = '%%-%ds' % (
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % get_builder(b)
450
451 def sort_key(b):
452 return b['status'], b.get('result'), get_name(b), b.get('url')
453
454 def pop(title, f, color=None, **kwargs):
455 """Pop matching builds from `builds` dict and print them."""
456
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000457 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 colorize = str
459 else:
460 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
461
462 result = []
463 for b in builds.values():
464 if all(b.get(k) == v for k, v in kwargs.iteritems()):
465 builds.pop(b['id'])
466 result.append(b)
467 if result:
468 print colorize(title)
469 for b in sorted(result, key=sort_key):
470 print ' ', colorize('\t'.join(map(str, f(b))))
471
472 total = len(builds)
473 pop(status='COMPLETED', result='SUCCESS',
474 title='Successes:', color=Fore.GREEN,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
477 title='Infra Failures:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
480 title='Failures:', color=Fore.RED,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='CANCELED',
483 title='Canceled:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 failure_reason='INVALID_BUILD_DEFINITION',
487 title='Wrong master/builder name:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 title='Other failures:',
491 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
492 pop(status='COMPLETED',
493 title='Other finished:',
494 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
495 pop(status='STARTED',
496 title='Started:', color=Fore.YELLOW,
497 f=lambda b: (get_name(b), b.get('url')))
498 pop(status='SCHEDULED',
499 title='Scheduled:',
500 f=lambda b: (get_name(b), 'id=%s' % b['id']))
501 # The last section is just in case buildbucket API changes OR there is a bug.
502 pop(title='Other:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 assert len(builds) == 0
505 print 'Total: %d tryjobs' % total
506
507
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000508def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
509 """Return the corresponding git ref if |base_url| together with |glob_spec|
510 matches the full |url|.
511
512 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
513 """
514 fetch_suburl, as_ref = glob_spec.split(':')
515 if allow_wildcards:
516 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
517 if glob_match:
518 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
519 # "branches/{472,597,648}/src:refs/remotes/svn/*".
520 branch_re = re.escape(base_url)
521 if glob_match.group(1):
522 branch_re += '/' + re.escape(glob_match.group(1))
523 wildcard = glob_match.group(2)
524 if wildcard == '*':
525 branch_re += '([^/]*)'
526 else:
527 # Escape and replace surrounding braces with parentheses and commas
528 # with pipe symbols.
529 wildcard = re.escape(wildcard)
530 wildcard = re.sub('^\\\\{', '(', wildcard)
531 wildcard = re.sub('\\\\,', '|', wildcard)
532 wildcard = re.sub('\\\\}$', ')', wildcard)
533 branch_re += wildcard
534 if glob_match.group(3):
535 branch_re += re.escape(glob_match.group(3))
536 match = re.match(branch_re, url)
537 if match:
538 return re.sub('\*$', match.group(1), as_ref)
539
540 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
541 if fetch_suburl:
542 full_url = base_url + '/' + fetch_suburl
543 else:
544 full_url = base_url
545 if full_url == url:
546 return as_ref
547 return None
548
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000549
iannucci@chromium.org79540052012-10-19 23:15:26 +0000550def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 """Prints statistics about the change to the user."""
552 # --no-ext-diff is broken in some versions of Git, so try to work around
553 # this by overriding the environment (but there is still a problem if the
554 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 if 'GIT_EXTERNAL_DIFF' in env:
557 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000558
559 if find_copies:
560 similarity_options = ['--find-copies-harder', '-l100000',
561 '-C%s' % similarity]
562 else:
563 similarity_options = ['-M%s' % similarity]
564
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000565 try:
566 stdout = sys.stdout.fileno()
567 except AttributeError:
568 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000570 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000571 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000572 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573
574
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000575class BuildbucketResponseException(Exception):
576 pass
577
578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579class Settings(object):
580 def __init__(self):
581 self.default_server = None
582 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000583 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 self.is_git_svn = None
585 self.svn_branch = None
586 self.tree_status_url = None
587 self.viewvc_url = None
588 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000589 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000590 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000591 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000592 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000594 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595
596 def LazyUpdateIfNeeded(self):
597 """Updates the settings from a codereview.settings file, if available."""
598 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000599 # The only value that actually changes the behavior is
600 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 error_ok=True
603 ).strip().lower()
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 LoadCodereviewSettingsFromFile(cr_settings_file)
608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000633 def GetGitMirror(self, remote='origin'):
634 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000635 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 if not os.path.isdir(local_url):
637 return None
638 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
639 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
640 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
641 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
642 if mirror.exists():
643 return mirror
644 return None
645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 def GetIsGitSvn(self):
647 """Return true if this repo looks like it's using git-svn."""
648 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000649 if self.GetPendingRefPrefix():
650 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
651 self.is_git_svn = False
652 else:
653 # If you have any "svn-remote.*" config keys, we think you're using svn.
654 self.is_git_svn = RunGitWithCode(
655 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return self.is_git_svn
657
658 def GetSVNBranch(self):
659 if self.svn_branch is None:
660 if not self.GetIsGitSvn():
661 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
662
663 # Try to figure out which remote branch we're based on.
664 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 # 1) iterate through our branch history and find the svn URL.
666 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 # regexp matching the git-svn line that contains the URL.
669 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # We don't want to go through all of history, so read a line from the
672 # pipe at a time.
673 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000674 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
676 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000677 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 for line in proc.stdout:
679 match = git_svn_re.match(line)
680 if match:
681 url = match.group(1)
682 proc.stdout.close() # Cut pipe.
683 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 if url:
686 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
687 remotes = RunGit(['config', '--get-regexp',
688 r'^svn-remote\..*\.url']).splitlines()
689 for remote in remotes:
690 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 remote = match.group(1)
693 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000694 rewrite_root = RunGit(
695 ['config', 'svn-remote.%s.rewriteRoot' % remote],
696 error_ok=True).strip()
697 if rewrite_root:
698 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000700 ['config', 'svn-remote.%s.fetch' % remote],
701 error_ok=True).strip()
702 if fetch_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
704 if self.svn_branch:
705 break
706 branch_spec = RunGit(
707 ['config', 'svn-remote.%s.branches' % remote],
708 error_ok=True).strip()
709 if branch_spec:
710 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
711 if self.svn_branch:
712 break
713 tag_spec = RunGit(
714 ['config', 'svn-remote.%s.tags' % remote],
715 error_ok=True).strip()
716 if tag_spec:
717 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
718 if self.svn_branch:
719 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 if not self.svn_branch:
722 DieWithError('Can\'t guess svn branch -- try specifying it on the '
723 'command line')
724
725 return self.svn_branch
726
727 def GetTreeStatusUrl(self, error_ok=False):
728 if not self.tree_status_url:
729 error_message = ('You must configure your tree status URL by running '
730 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000731 self.tree_status_url = self._GetRietveldConfig(
732 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.tree_status_url
734
735 def GetViewVCUrl(self):
736 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.viewvc_url
739
rmistry@google.com90752582014-01-14 21:04:50 +0000740 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000742
rmistry@google.com78948ed2015-07-08 23:09:57 +0000743 def GetIsSkipDependencyUpload(self, branch_name):
744 """Returns true if specified branch should skip dep uploads."""
745 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
746 error_ok=True)
747
rmistry@google.com5626a922015-02-26 14:03:30 +0000748 def GetRunPostUploadHook(self):
749 run_post_upload_hook = self._GetRietveldConfig(
750 'run-post-upload-hook', error_ok=True)
751 return run_post_upload_hook == "True"
752
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000753 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000756 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 def GetIsGerrit(self):
760 """Return true if this repo is assosiated with gerrit code review system."""
761 if self.is_gerrit is None:
762 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
763 return self.is_gerrit
764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 def GetSquashGerritUploads(self):
766 """Return true if uploads to Gerrit should be squashed by default."""
767 if self.squash_gerrit_uploads is None:
768 self.squash_gerrit_uploads = (
769 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
770 error_ok=True).strip() == 'true')
771 return self.squash_gerrit_uploads
772
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 def GetGitEditor(self):
774 """Return the editor specified in the git config, or None if none is."""
775 if self.git_editor is None:
776 self.git_editor = self._GetConfig('core.editor', error_ok=True)
777 return self.git_editor or None
778
thestig@chromium.org44202a22014-03-11 19:22:18 +0000779 def GetLintRegex(self):
780 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
781 DEFAULT_LINT_REGEX)
782
783 def GetLintIgnoreRegex(self):
784 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
785 DEFAULT_LINT_IGNORE_REGEX)
786
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000787 def GetProject(self):
788 if not self.project:
789 self.project = self._GetRietveldConfig('project', error_ok=True)
790 return self.project
791
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000792 def GetForceHttpsCommitUrl(self):
793 if not self.force_https_commit_url:
794 self.force_https_commit_url = self._GetRietveldConfig(
795 'force-https-commit-url', error_ok=True)
796 return self.force_https_commit_url
797
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000798 def GetPendingRefPrefix(self):
799 if not self.pending_ref_prefix:
800 self.pending_ref_prefix = self._GetRietveldConfig(
801 'pending-ref-prefix', error_ok=True)
802 return self.pending_ref_prefix
803
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 def _GetRietveldConfig(self, param, **kwargs):
805 return self._GetConfig('rietveld.' + param, **kwargs)
806
rmistry@google.com78948ed2015-07-08 23:09:57 +0000807 def _GetBranchConfig(self, branch_name, param, **kwargs):
808 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def _GetConfig(self, param, **kwargs):
811 self.LazyUpdateIfNeeded()
812 return RunGit(['config', param], **kwargs).strip()
813
814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815def ShortBranchName(branch):
816 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000817 return branch.replace('refs/heads/', '', 1)
818
819
820def GetCurrentBranchRef():
821 """Returns branch ref (e.g., refs/heads/master) or None."""
822 return RunGit(['symbolic-ref', 'HEAD'],
823 stderr=subprocess2.VOID, error_ok=True).strip() or None
824
825
826def GetCurrentBranch():
827 """Returns current branch or None.
828
829 For refs/heads/* branches, returns just last part. For others, full ref.
830 """
831 branchref = GetCurrentBranchRef()
832 if branchref:
833 return ShortBranchName(branchref)
834 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836
837class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000838 """Changelist works with one changelist in local branch.
839
840 Supports two codereview backends: Rietveld or Gerrit, selected at object
841 creation.
842
843 Not safe for concurrent multi-{thread,process} use.
844 """
845
846 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
847 """Create a new ChangeList instance.
848
849 If issue is given, the codereview must be given too.
850
851 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
852 Otherwise, it's decided based on current configuration of the local branch,
853 with default being 'rietveld' for backwards compatibility.
854 See _load_codereview_impl for more details.
855
856 **kwargs will be passed directly to codereview implementation.
857 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000859 global settings
860 if not settings:
861 # Happens when git_cl.py is used as a utility library.
862 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000863
864 if issue:
865 assert codereview, 'codereview must be known, if issue is known'
866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 self.branchref = branchref
868 if self.branchref:
869 self.branch = ShortBranchName(self.branchref)
870 else:
871 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000873 self.lookedup_issue = False
874 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 self.has_description = False
876 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000877 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000879 self.cc = None
880 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000881 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000882
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000883 self._codereview_impl = None
884 self._load_codereview_impl(codereview, **kwargs)
885
886 def _load_codereview_impl(self, codereview=None, **kwargs):
887 if codereview:
888 codereview = codereview.lower()
889 if codereview == 'gerrit':
890 self._codereview_impl = _GerritChangelistImpl(self, **kwargs)
891 elif codereview == 'rietveld':
892 self._codereview_impl = _RietveldChangelistImpl(self, **kwargs)
893 else:
894 assert codereview in ('rietveld', 'gerrit')
895 return
896
897 # Automatic selection based on issue number set for a current branch.
898 # Rietveld takes precedence over Gerrit.
899 assert not self.issue
900 # Whether we find issue or not, we are doing the lookup.
901 self.lookedup_issue = True
902 for cls in [_RietveldChangelistImpl, _GerritChangelistImpl]:
903 setting = cls.IssueSetting(self.GetBranch())
904 issue = RunGit(['config', setting], error_ok=True).strip()
905 if issue:
906 self._codereview_impl = cls(self, **kwargs)
907 self.issue = int(issue)
908 return
909
910 # No issue is set for this branch, so decide based on repo-wide settings.
911 return self._load_codereview_impl(
912 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
913 **kwargs)
914
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000915
916 def GetCCList(self):
917 """Return the users cc'd on this CL.
918
919 Return is a string suitable for passing to gcl with the --cc flag.
920 """
921 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000922 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000923 more_cc = ','.join(self.watchers)
924 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
925 return self.cc
926
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000927 def GetCCListWithoutDefault(self):
928 """Return the users cc'd on this CL excluding default ones."""
929 if self.cc is None:
930 self.cc = ','.join(self.watchers)
931 return self.cc
932
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000933 def SetWatchers(self, watchers):
934 """Set the list of email addresses that should be cc'd based on the changed
935 files in this CL.
936 """
937 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
939 def GetBranch(self):
940 """Returns the short branch name, e.g. 'master'."""
941 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000942 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000943 if not branchref:
944 return None
945 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.branch = ShortBranchName(self.branchref)
947 return self.branch
948
949 def GetBranchRef(self):
950 """Returns the full branch name, e.g. 'refs/heads/master'."""
951 self.GetBranch() # Poke the lazy loader.
952 return self.branchref
953
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000954 @staticmethod
955 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000956 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 e.g. 'origin', 'refs/heads/master'
958 """
959 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
961 error_ok=True).strip()
962 if upstream_branch:
963 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
964 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000965 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
966 error_ok=True).strip()
967 if upstream_branch:
968 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000970 # Fall back on trying a git-svn upstream branch.
971 if settings.GetIsGitSvn():
972 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000974 # Else, try to guess the origin remote.
975 remote_branches = RunGit(['branch', '-r']).split()
976 if 'origin/master' in remote_branches:
977 # Fall back on origin/master if it exits.
978 remote = 'origin'
979 upstream_branch = 'refs/heads/master'
980 elif 'origin/trunk' in remote_branches:
981 # Fall back on origin/trunk if it exists. Generally a shared
982 # git-svn clone
983 remote = 'origin'
984 upstream_branch = 'refs/heads/trunk'
985 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000986 DieWithError(
987 'Unable to determine default branch to diff against.\n'
988 'Either pass complete "git diff"-style arguments, like\n'
989 ' git cl upload origin/master\n'
990 'or verify this branch is set up to track another \n'
991 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992
993 return remote, upstream_branch
994
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000995 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000996 upstream_branch = self.GetUpstreamBranch()
997 if not BranchExists(upstream_branch):
998 DieWithError('The upstream for the current branch (%s) does not exist '
999 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001000 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001001 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003 def GetUpstreamBranch(self):
1004 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001005 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001007 upstream_branch = upstream_branch.replace('refs/heads/',
1008 'refs/remotes/%s/' % remote)
1009 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1010 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.upstream_branch = upstream_branch
1012 return self.upstream_branch
1013
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001014 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001015 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001016 remote, branch = None, self.GetBranch()
1017 seen_branches = set()
1018 while branch not in seen_branches:
1019 seen_branches.add(branch)
1020 remote, branch = self.FetchUpstreamTuple(branch)
1021 branch = ShortBranchName(branch)
1022 if remote != '.' or branch.startswith('refs/remotes'):
1023 break
1024 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001025 remotes = RunGit(['remote'], error_ok=True).split()
1026 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001027 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001028 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001029 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001030 logging.warning('Could not determine which remote this change is '
1031 'associated with, so defaulting to "%s". This may '
1032 'not be what you want. You may prevent this message '
1033 'by running "git svn info" as documented here: %s',
1034 self._remote,
1035 GIT_INSTRUCTIONS_URL)
1036 else:
1037 logging.warn('Could not determine which remote this change is '
1038 'associated with. You may prevent this message by '
1039 'running "git svn info" as documented here: %s',
1040 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001041 branch = 'HEAD'
1042 if branch.startswith('refs/remotes'):
1043 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001044 elif branch.startswith('refs/branch-heads/'):
1045 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001046 else:
1047 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001048 return self._remote
1049
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 def GitSanityChecks(self, upstream_git_obj):
1051 """Checks git repo status and ensures diff is from local commits."""
1052
sbc@chromium.org79706062015-01-14 21:18:12 +00001053 if upstream_git_obj is None:
1054 if self.GetBranch() is None:
1055 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001056 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001057 else:
1058 print >> sys.stderr, (
1059 'ERROR: no upstream branch')
1060 return False
1061
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001062 # Verify the commit we're diffing against is in our current branch.
1063 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1064 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1065 if upstream_sha != common_ancestor:
1066 print >> sys.stderr, (
1067 'ERROR: %s is not in the current branch. You may need to rebase '
1068 'your tracking branch' % upstream_sha)
1069 return False
1070
1071 # List the commits inside the diff, and verify they are all local.
1072 commits_in_diff = RunGit(
1073 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1074 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1075 remote_branch = remote_branch.strip()
1076 if code != 0:
1077 _, remote_branch = self.GetRemoteBranch()
1078
1079 commits_in_remote = RunGit(
1080 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1081
1082 common_commits = set(commits_in_diff) & set(commits_in_remote)
1083 if common_commits:
1084 print >> sys.stderr, (
1085 'ERROR: Your diff contains %d commits already in %s.\n'
1086 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1087 'the diff. If you are using a custom git flow, you can override'
1088 ' the reference used for this check with "git config '
1089 'gitcl.remotebranch <git-ref>".' % (
1090 len(common_commits), remote_branch, upstream_git_obj))
1091 return False
1092 return True
1093
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001094 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001095 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001096
1097 Returns None if it is not set.
1098 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001099 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1100 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001101
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001102 def GetGitSvnRemoteUrl(self):
1103 """Return the configured git-svn remote URL parsed from git svn info.
1104
1105 Returns None if it is not set.
1106 """
1107 # URL is dependent on the current directory.
1108 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1109 if data:
1110 keys = dict(line.split(': ', 1) for line in data.splitlines()
1111 if ': ' in line)
1112 return keys.get('URL', None)
1113 return None
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 def GetRemoteUrl(self):
1116 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1117
1118 Returns None if there is no remote.
1119 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001120 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001121 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1122
1123 # If URL is pointing to a local directory, it is probably a git cache.
1124 if os.path.isdir(url):
1125 url = RunGit(['config', 'remote.%s.url' % remote],
1126 error_ok=True,
1127 cwd=url).strip()
1128 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001130 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001132 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001133 issue = RunGit(['config',
1134 self._codereview_impl.IssueSetting(self.GetBranch())],
1135 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.issue = int(issue) or None if issue else None
1137 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 return self.issue
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 def GetIssueURL(self):
1141 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142 issue = self.GetIssue()
1143 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001144 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146
1147 def GetDescription(self, pretty=False):
1148 if not self.has_description:
1149 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.has_description = True
1152 if pretty:
1153 wrapper = textwrap.TextWrapper()
1154 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1155 return wrapper.fill(self.description)
1156 return self.description
1157
1158 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001159 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 self.patchset = int(patchset) or None if patchset else None
1164 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return self.patchset
1166
1167 def SetPatchset(self, patchset):
1168 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001175 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001176 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001178 def SetIssue(self, issue=None):
1179 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1181 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001183 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184 RunGit(['config', issue_setting, str(issue)])
1185 codereview_server = self._codereview_impl.GetCodereviewServer()
1186 if codereview_server:
1187 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001189 current_issue = self.GetIssue()
1190 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001192 self.issue = None
1193 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001195 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 if not self.GitSanityChecks(upstream_branch):
1197 DieWithError('\nGit sanity check failure')
1198
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001200 if not root:
1201 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001202 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001203
1204 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001205 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001206 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001207 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001208 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001209 except subprocess2.CalledProcessError:
1210 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001211 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001212 'This branch probably doesn\'t exist anymore. To reset the\n'
1213 'tracking branch, please run\n'
1214 ' git branch --set-upstream %s trunk\n'
1215 'replacing trunk with origin/master or the relevant branch') %
1216 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001217
maruel@chromium.org52424302012-08-29 15:14:30 +00001218 issue = self.GetIssue()
1219 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001220 if issue:
1221 description = self.GetDescription()
1222 else:
1223 # If the change was never uploaded, use the log messages of all commits
1224 # up to the branch point, as git cl upload will prefill the description
1225 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001226 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1227 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001228
1229 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001230 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001231 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001232 name,
1233 description,
1234 absroot,
1235 files,
1236 issue,
1237 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001238 author,
1239 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001240
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 def UpdateDescription(self, description):
1242 self.description = description
1243 return self._codereview_impl.UpdateDescriptionRemote(description)
1244
1245 def RunHook(self, committing, may_prompt, verbose, change):
1246 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1247 try:
1248 return presubmit_support.DoPresubmitChecks(change, committing,
1249 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1250 default_presubmit=None, may_prompt=may_prompt,
1251 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1252 except presubmit_support.PresubmitFailure, e:
1253 DieWithError(
1254 ('%s\nMaybe your depot_tools is out of date?\n'
1255 'If all fails, contact maruel@') % e)
1256
1257 # Forward methods to codereview specific implementation.
1258
1259 def CloseIssue(self):
1260 return self._codereview_impl.CloseIssue()
1261
1262 def GetStatus(self):
1263 return self._codereview_impl.GetStatus()
1264
1265 def GetCodereviewServer(self):
1266 return self._codereview_impl.GetCodereviewServer()
1267
1268 def GetApprovingReviewers(self):
1269 return self._codereview_impl.GetApprovingReviewers()
1270
1271 def GetMostRecentPatchset(self):
1272 return self._codereview_impl.GetMostRecentPatchset()
1273
1274 def __getattr__(self, attr):
1275 # This is because lots of untested code accesses Rietveld-specific stuff
1276 # directly, and it's hard to fix for sure. So, just let it work, and fix
1277 # on a cases by case basis.
1278 return getattr(self._codereview_impl, attr)
1279
1280
1281class _ChangelistCodereviewBase(object):
1282 """Abstract base class encapsulating codereview specifics of a changelist."""
1283 def __init__(self, changelist):
1284 self._changelist = changelist # instance of Changelist
1285
1286 def __getattr__(self, attr):
1287 # Forward methods to changelist.
1288 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1289 # _RietveldChangelistImpl to avoid this hack?
1290 return getattr(self._changelist, attr)
1291
1292 def GetStatus(self):
1293 """Apply a rough heuristic to give a simple summary of an issue's review
1294 or CQ status, assuming adherence to a common workflow.
1295
1296 Returns None if no issue for this branch, or specific string keywords.
1297 """
1298 raise NotImplementedError()
1299
1300 def GetCodereviewServer(self):
1301 """Returns server URL without end slash, like "https://codereview.com"."""
1302 raise NotImplementedError()
1303
1304 def FetchDescription(self):
1305 """Fetches and returns description from the codereview server."""
1306 raise NotImplementedError()
1307
1308 def GetCodereviewServerSetting(self):
1309 """Returns git config setting for the codereview server."""
1310 raise NotImplementedError()
1311
1312 @staticmethod
1313 def IssueSetting(branch):
1314 """Returns name of git config setting which stores issue number for a given
1315 branch."""
1316 raise NotImplementedError()
1317
1318 def PatchsetSetting(self):
1319 """Returns name of git config setting which stores issue number."""
1320 raise NotImplementedError()
1321
1322 def GetRieveldObjForPresubmit(self):
1323 # This is an unfortunate Rietveld-embeddedness in presubmit.
1324 # For non-Rietveld codereviews, this probably should return a dummy object.
1325 raise NotImplementedError()
1326
1327 def UpdateDescriptionRemote(self, description):
1328 """Update the description on codereview site."""
1329 raise NotImplementedError()
1330
1331 def CloseIssue(self):
1332 """Closes the issue."""
1333 raise NotImplementedError()
1334
1335 def GetApprovingReviewers(self):
1336 """Returns a list of reviewers approving the change.
1337
1338 Note: not necessarily committers.
1339 """
1340 raise NotImplementedError()
1341
1342 def GetMostRecentPatchset(self):
1343 """Returns the most recent patchset number from the codereview site."""
1344 raise NotImplementedError()
1345
1346
1347class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1348 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1349 super(_RietveldChangelistImpl, self).__init__(changelist)
1350 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1351 settings.GetDefaultServerUrl()
1352
1353 self._rietveld_server = rietveld_server
1354 self._auth_config = auth_config
1355 self._props = None
1356 self._rpc_server = None
1357
1358 def GetAuthConfig(self):
1359 return self._auth_config
1360
1361 def GetCodereviewServer(self):
1362 if not self._rietveld_server:
1363 # If we're on a branch then get the server potentially associated
1364 # with that branch.
1365 if self.GetIssue():
1366 rietveld_server_setting = self.GetCodereviewServerSetting()
1367 if rietveld_server_setting:
1368 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1369 ['config', rietveld_server_setting], error_ok=True).strip())
1370 if not self._rietveld_server:
1371 self._rietveld_server = settings.GetDefaultServerUrl()
1372 return self._rietveld_server
1373
1374 def FetchDescription(self):
1375 issue = self.GetIssue()
1376 assert issue
1377 try:
1378 return self.RpcServer().get_description(issue).strip()
1379 except urllib2.HTTPError as e:
1380 if e.code == 404:
1381 DieWithError(
1382 ('\nWhile fetching the description for issue %d, received a '
1383 '404 (not found)\n'
1384 'error. It is likely that you deleted this '
1385 'issue on the server. If this is the\n'
1386 'case, please run\n\n'
1387 ' git cl issue 0\n\n'
1388 'to clear the association with the deleted issue. Then run '
1389 'this command again.') % issue)
1390 else:
1391 DieWithError(
1392 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1393 except urllib2.URLError as e:
1394 print >> sys.stderr, (
1395 'Warning: Failed to retrieve CL description due to network '
1396 'failure.')
1397 return ''
1398
1399 def GetMostRecentPatchset(self):
1400 return self.GetIssueProperties()['patchsets'][-1]
1401
1402 def GetPatchSetDiff(self, issue, patchset):
1403 return self.RpcServer().get(
1404 '/download/issue%s_%s.diff' % (issue, patchset))
1405
1406 def GetIssueProperties(self):
1407 if self._props is None:
1408 issue = self.GetIssue()
1409 if not issue:
1410 self._props = {}
1411 else:
1412 self._props = self.RpcServer().get_issue_properties(issue, True)
1413 return self._props
1414
1415 def GetApprovingReviewers(self):
1416 return get_approving_reviewers(self.GetIssueProperties())
1417
1418 def AddComment(self, message):
1419 return self.RpcServer().add_comment(self.GetIssue(), message)
1420
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001421 def GetStatus(self):
1422 """Apply a rough heuristic to give a simple summary of an issue's review
1423 or CQ status, assuming adherence to a common workflow.
1424
1425 Returns None if no issue for this branch, or one of the following keywords:
1426 * 'error' - error from review tool (including deleted issues)
1427 * 'unsent' - not sent for review
1428 * 'waiting' - waiting for review
1429 * 'reply' - waiting for owner to reply to review
1430 * 'lgtm' - LGTM from at least one approved reviewer
1431 * 'commit' - in the commit queue
1432 * 'closed' - closed
1433 """
1434 if not self.GetIssue():
1435 return None
1436
1437 try:
1438 props = self.GetIssueProperties()
1439 except urllib2.HTTPError:
1440 return 'error'
1441
1442 if props.get('closed'):
1443 # Issue is closed.
1444 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001445 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001446 # Issue is in the commit queue.
1447 return 'commit'
1448
1449 try:
1450 reviewers = self.GetApprovingReviewers()
1451 except urllib2.HTTPError:
1452 return 'error'
1453
1454 if reviewers:
1455 # Was LGTM'ed.
1456 return 'lgtm'
1457
1458 messages = props.get('messages') or []
1459
1460 if not messages:
1461 # No message was sent.
1462 return 'unsent'
1463 if messages[-1]['sender'] != props.get('owner_email'):
1464 # Non-LGTM reply from non-owner
1465 return 'reply'
1466 return 'waiting'
1467
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001468 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001469 return self.RpcServer().update_description(
1470 self.GetIssue(), self.description)
1471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001473 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001475 def SetFlag(self, flag, value):
1476 """Patchset must match."""
1477 if not self.GetPatchset():
1478 DieWithError('The patchset needs to match. Send another patchset.')
1479 try:
1480 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001481 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001482 except urllib2.HTTPError, e:
1483 if e.code == 404:
1484 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1485 if e.code == 403:
1486 DieWithError(
1487 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1488 'match?') % (self.GetIssue(), self.GetPatchset()))
1489 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001491 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 """Returns an upload.RpcServer() to access this review's rietveld instance.
1493 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001494 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001495 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001497 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001498 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001500 @staticmethod
1501 def IssueSetting(branch):
1502 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001504 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505 """Return the git setting that stores this change's most recent patchset."""
1506 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1507
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001510 branch = self.GetBranch()
1511 if branch:
1512 return 'branch.%s.rietveldserver' % branch
1513 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001515 def GetRieveldObjForPresubmit(self):
1516 return self.RpcServer()
1517
1518
1519class _GerritChangelistImpl(_ChangelistCodereviewBase):
1520 def __init__(self, changelist, auth_config=None):
1521 # auth_config is Rietveld thing, kept here to preserve interface only.
1522 super(_GerritChangelistImpl, self).__init__(changelist)
1523 self._change_id = None
1524 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1525 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1526
1527 def _GetGerritHost(self):
1528 # Lazy load of configs.
1529 self.GetCodereviewServer()
1530 return self._gerrit_host
1531
1532 def GetCodereviewServer(self):
1533 if not self._gerrit_server:
1534 # If we're on a branch then get the server potentially associated
1535 # with that branch.
1536 if self.GetIssue():
1537 gerrit_server_setting = self.GetCodereviewServerSetting()
1538 if gerrit_server_setting:
1539 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1540 error_ok=True).strip()
1541 if self._gerrit_server:
1542 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1543 if not self._gerrit_server:
1544 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1545 # has "-review" suffix for lowest level subdomain.
1546 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1547 parts[0] = parts[0] + '-review'
1548 self._gerrit_host = '.'.join(parts)
1549 self._gerrit_server = 'https://%s' % self._gerrit_host
1550 return self._gerrit_server
1551
1552 @staticmethod
1553 def IssueSetting(branch):
1554 return 'branch.%s.gerritissue' % branch
1555
1556 def PatchsetSetting(self):
1557 """Return the git setting that stores this change's most recent patchset."""
1558 return 'branch.%s.gerritpatchset' % self.GetBranch()
1559
1560 def GetCodereviewServerSetting(self):
1561 """Returns the git setting that stores this change's Gerrit server."""
1562 branch = self.GetBranch()
1563 if branch:
1564 return 'branch.%s.gerritserver' % branch
1565 return None
1566
1567 def GetRieveldObjForPresubmit(self):
1568 class ThisIsNotRietveldIssue(object):
1569 def __nonzero__(self):
1570 # This is a hack to make presubmit_support think that rietveld is not
1571 # defined, yet still ensure that calls directly result in a decent
1572 # exception message below.
1573 return False
1574
1575 def __getattr__(self, attr):
1576 print(
1577 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1578 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1579 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1580 'or use Rietveld for codereview.\n'
1581 'See also http://crbug.com/579160.' % attr)
1582 raise NotImplementedError()
1583 return ThisIsNotRietveldIssue()
1584
1585 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001586 """Apply a rough heuristic to give a simple summary of an issue's review
1587 or CQ status, assuming adherence to a common workflow.
1588
1589 Returns None if no issue for this branch, or one of the following keywords:
1590 * 'error' - error from review tool (including deleted issues)
1591 * 'unsent' - no reviewers added
1592 * 'waiting' - waiting for review
1593 * 'reply' - waiting for owner to reply to review
1594 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1595 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1596 * 'commit' - in the commit queue
1597 * 'closed' - abandoned
1598 """
1599 if not self.GetIssue():
1600 return None
1601
1602 try:
1603 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1604 except httplib.HTTPException:
1605 return 'error'
1606
1607 if data['status'] == 'ABANDONED':
1608 return 'closed'
1609
1610 cq_label = data['labels'].get('Commit-Queue', {})
1611 if cq_label:
1612 # Vote value is a stringified integer, which we expect from 0 to 2.
1613 vote_value = cq_label.get('value', '0')
1614 vote_text = cq_label.get('values', {}).get(vote_value, '')
1615 if vote_text.lower() == 'commit':
1616 return 'commit'
1617
1618 lgtm_label = data['labels'].get('Code-Review', {})
1619 if lgtm_label:
1620 if 'rejected' in lgtm_label:
1621 return 'not lgtm'
1622 if 'approved' in lgtm_label:
1623 return 'lgtm'
1624
1625 if not data.get('reviewers', {}).get('REVIEWER', []):
1626 return 'unsent'
1627
1628 messages = data.get('messages', [])
1629 if messages:
1630 owner = data['owner'].get('_account_id')
1631 last_message_author = messages[-1].get('author', {}).get('_account_id')
1632 if owner != last_message_author:
1633 # Some reply from non-owner.
1634 return 'reply'
1635
1636 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001637
1638 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001639 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001640 return data['revisions'][data['current_revision']]['_number']
1641
1642 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001643 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 return data['revisions'][data['current_revision']]['commit_with_footers']
1645
1646 def UpdateDescriptionRemote(self, description):
1647 # TODO(tandrii)
1648 raise NotImplementedError()
1649
1650 def CloseIssue(self):
1651 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001654 def _GetChangeDetail(self, options):
1655 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1656 options)
1657
1658
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001659class ChangeDescription(object):
1660 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001661 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001662 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001663
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001664 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001665 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001666
agable@chromium.org42c20792013-09-12 17:34:49 +00001667 @property # www.logilab.org/ticket/89786
1668 def description(self): # pylint: disable=E0202
1669 return '\n'.join(self._description_lines)
1670
1671 def set_description(self, desc):
1672 if isinstance(desc, basestring):
1673 lines = desc.splitlines()
1674 else:
1675 lines = [line.rstrip() for line in desc]
1676 while lines and not lines[0]:
1677 lines.pop(0)
1678 while lines and not lines[-1]:
1679 lines.pop(-1)
1680 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001681
piman@chromium.org336f9122014-09-04 02:16:55 +00001682 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001683 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001684 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001685 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001686 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001687 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001688
agable@chromium.org42c20792013-09-12 17:34:49 +00001689 # Get the set of R= and TBR= lines and remove them from the desciption.
1690 regexp = re.compile(self.R_LINE)
1691 matches = [regexp.match(line) for line in self._description_lines]
1692 new_desc = [l for i, l in enumerate(self._description_lines)
1693 if not matches[i]]
1694 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001695
agable@chromium.org42c20792013-09-12 17:34:49 +00001696 # Construct new unified R= and TBR= lines.
1697 r_names = []
1698 tbr_names = []
1699 for match in matches:
1700 if not match:
1701 continue
1702 people = cleanup_list([match.group(2).strip()])
1703 if match.group(1) == 'TBR':
1704 tbr_names.extend(people)
1705 else:
1706 r_names.extend(people)
1707 for name in r_names:
1708 if name not in reviewers:
1709 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001710 if add_owners_tbr:
1711 owners_db = owners.Database(change.RepositoryRoot(),
1712 fopen=file, os_path=os.path, glob=glob.glob)
1713 all_reviewers = set(tbr_names + reviewers)
1714 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1715 all_reviewers)
1716 tbr_names.extend(owners_db.reviewers_for(missing_files,
1717 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001718 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1719 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1720
1721 # Put the new lines in the description where the old first R= line was.
1722 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1723 if 0 <= line_loc < len(self._description_lines):
1724 if new_tbr_line:
1725 self._description_lines.insert(line_loc, new_tbr_line)
1726 if new_r_line:
1727 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001728 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001729 if new_r_line:
1730 self.append_footer(new_r_line)
1731 if new_tbr_line:
1732 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001733
1734 def prompt(self):
1735 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001736 self.set_description([
1737 '# Enter a description of the change.',
1738 '# This will be displayed on the codereview site.',
1739 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001740 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001741 '--------------------',
1742 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001743
agable@chromium.org42c20792013-09-12 17:34:49 +00001744 regexp = re.compile(self.BUG_LINE)
1745 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001746 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001747 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001748 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001749 if not content:
1750 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001751 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001752
1753 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001754 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1755 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001756 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001757 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001758
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001759 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001760 if self._description_lines:
1761 # Add an empty line if either the last line or the new line isn't a tag.
1762 last_line = self._description_lines[-1]
1763 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1764 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1765 self._description_lines.append('')
1766 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001767
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001768 def get_reviewers(self):
1769 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001770 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1771 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001772 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001773
1774
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001775def get_approving_reviewers(props):
1776 """Retrieves the reviewers that approved a CL from the issue properties with
1777 messages.
1778
1779 Note that the list may contain reviewers that are not committer, thus are not
1780 considered by the CQ.
1781 """
1782 return sorted(
1783 set(
1784 message['sender']
1785 for message in props['messages']
1786 if message['approval'] and message['sender'] in props['reviewers']
1787 )
1788 )
1789
1790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791def FindCodereviewSettingsFile(filename='codereview.settings'):
1792 """Finds the given file starting in the cwd and going up.
1793
1794 Only looks up to the top of the repository unless an
1795 'inherit-review-settings-ok' file exists in the root of the repository.
1796 """
1797 inherit_ok_file = 'inherit-review-settings-ok'
1798 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001799 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1801 root = '/'
1802 while True:
1803 if filename in os.listdir(cwd):
1804 if os.path.isfile(os.path.join(cwd, filename)):
1805 return open(os.path.join(cwd, filename))
1806 if cwd == root:
1807 break
1808 cwd = os.path.dirname(cwd)
1809
1810
1811def LoadCodereviewSettingsFromFile(fileobj):
1812 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001813 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 def SetProperty(name, setting, unset_error_ok=False):
1816 fullname = 'rietveld.' + name
1817 if setting in keyvals:
1818 RunGit(['config', fullname, keyvals[setting]])
1819 else:
1820 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1821
1822 SetProperty('server', 'CODE_REVIEW_SERVER')
1823 # Only server setting is required. Other settings can be absent.
1824 # In that case, we ignore errors raised during option deletion attempt.
1825 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001826 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1828 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001829 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001830 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001831 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1832 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001833 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001834 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001835 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001836 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1837 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001839 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001840 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001841
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001842 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1843 RunGit(['config', 'gerrit.squash-uploads',
1844 keyvals['GERRIT_SQUASH_UPLOADS']])
1845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1847 #should be of the form
1848 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1849 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1850 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1851 keyvals['ORIGIN_URL_CONFIG']])
1852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001854def urlretrieve(source, destination):
1855 """urllib is broken for SSL connections via a proxy therefore we
1856 can't use urllib.urlretrieve()."""
1857 with open(destination, 'w') as f:
1858 f.write(urllib2.urlopen(source).read())
1859
1860
ukai@chromium.org712d6102013-11-27 00:52:58 +00001861def hasSheBang(fname):
1862 """Checks fname is a #! script."""
1863 with open(fname) as f:
1864 return f.read(2).startswith('#!')
1865
1866
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001867def DownloadGerritHook(force):
1868 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001869
1870 Args:
1871 force: True to update hooks. False to install hooks if not present.
1872 """
1873 if not settings.GetIsGerrit():
1874 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001875 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001876 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1877 if not os.access(dst, os.X_OK):
1878 if os.path.exists(dst):
1879 if not force:
1880 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001881 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001882 print(
1883 'WARNING: installing Gerrit commit-msg hook.\n'
1884 ' This behavior of git cl will soon be disabled.\n'
1885 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001886 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001887 if not hasSheBang(dst):
1888 DieWithError('Not a script: %s\n'
1889 'You need to download from\n%s\n'
1890 'into .git/hooks/commit-msg and '
1891 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001892 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1893 except Exception:
1894 if os.path.exists(dst):
1895 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001896 DieWithError('\nFailed to download hooks.\n'
1897 'You need to download from\n%s\n'
1898 'into .git/hooks/commit-msg and '
1899 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001900
1901
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001902
1903def GetRietveldCodereviewSettingsInteractively():
1904 """Prompt the user for settings."""
1905 server = settings.GetDefaultServerUrl(error_ok=True)
1906 prompt = 'Rietveld server (host[:port])'
1907 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1908 newserver = ask_for_data(prompt + ':')
1909 if not server and not newserver:
1910 newserver = DEFAULT_SERVER
1911 if newserver:
1912 newserver = gclient_utils.UpgradeToHttps(newserver)
1913 if newserver != server:
1914 RunGit(['config', 'rietveld.server', newserver])
1915
1916 def SetProperty(initial, caption, name, is_url):
1917 prompt = caption
1918 if initial:
1919 prompt += ' ("x" to clear) [%s]' % initial
1920 new_val = ask_for_data(prompt + ':')
1921 if new_val == 'x':
1922 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1923 elif new_val:
1924 if is_url:
1925 new_val = gclient_utils.UpgradeToHttps(new_val)
1926 if new_val != initial:
1927 RunGit(['config', 'rietveld.' + name, new_val])
1928
1929 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1930 SetProperty(settings.GetDefaultPrivateFlag(),
1931 'Private flag (rietveld only)', 'private', False)
1932 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1933 'tree-status-url', False)
1934 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1935 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1936 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1937 'run-post-upload-hook', False)
1938
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001939@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001941 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001943 print('WARNING: git cl config works for Rietveld only.\n'
1944 'For Gerrit, see http://crbug.com/579160.')
1945 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001946 parser.add_option('--activate-update', action='store_true',
1947 help='activate auto-updating [rietveld] section in '
1948 '.git/config')
1949 parser.add_option('--deactivate-update', action='store_true',
1950 help='deactivate auto-updating [rietveld] section in '
1951 '.git/config')
1952 options, args = parser.parse_args(args)
1953
1954 if options.deactivate_update:
1955 RunGit(['config', 'rietveld.autoupdate', 'false'])
1956 return
1957
1958 if options.activate_update:
1959 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1960 return
1961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001963 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964 return 0
1965
1966 url = args[0]
1967 if not url.endswith('codereview.settings'):
1968 url = os.path.join(url, 'codereview.settings')
1969
1970 # Load code review settings and download hooks (if available).
1971 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1972 return 0
1973
1974
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001975def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001976 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001977 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1978 branch = ShortBranchName(branchref)
1979 _, args = parser.parse_args(args)
1980 if not args:
1981 print("Current base-url:")
1982 return RunGit(['config', 'branch.%s.base-url' % branch],
1983 error_ok=False).strip()
1984 else:
1985 print("Setting base-url to %s" % args[0])
1986 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1987 error_ok=False).strip()
1988
1989
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001990def color_for_status(status):
1991 """Maps a Changelist status to color, for CMDstatus and other tools."""
1992 return {
1993 'unsent': Fore.RED,
1994 'waiting': Fore.BLUE,
1995 'reply': Fore.YELLOW,
1996 'lgtm': Fore.GREEN,
1997 'commit': Fore.MAGENTA,
1998 'closed': Fore.CYAN,
1999 'error': Fore.WHITE,
2000 }.get(status, Fore.WHITE)
2001
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002002def fetch_cl_status(branch, auth_config=None):
2003 """Fetches information for an issue and returns (branch, issue, status)."""
2004 cl = Changelist(branchref=branch, auth_config=auth_config)
2005 url = cl.GetIssueURL()
2006 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002007
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002008 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002009 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002010 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002011
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002012 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002013
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002014def get_cl_statuses(
2015 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002016 """Returns a blocking iterable of (branch, issue, color) for given branches.
2017
2018 If fine_grained is true, this will fetch CL statuses from the server.
2019 Otherwise, simply indicate if there's a matching url for the given branches.
2020
2021 If max_processes is specified, it is used as the maximum number of processes
2022 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2023 spawned.
2024 """
2025 # Silence upload.py otherwise it becomes unwieldly.
2026 upload.verbosity = 0
2027
2028 if fine_grained:
2029 # Process one branch synchronously to work through authentication, then
2030 # spawn processes to process all the other branches in parallel.
2031 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002032 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2033 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002034
2035 branches_to_fetch = branches[1:]
2036 pool = ThreadPool(
2037 min(max_processes, len(branches_to_fetch))
2038 if max_processes is not None
2039 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002040 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002041 yield x
2042 else:
2043 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2044 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002045 cl = Changelist(branchref=b, auth_config=auth_config)
2046 url = cl.GetIssueURL()
2047 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002048
rmistry@google.com2dd99862015-06-22 12:22:18 +00002049
2050def upload_branch_deps(cl, args):
2051 """Uploads CLs of local branches that are dependents of the current branch.
2052
2053 If the local branch dependency tree looks like:
2054 test1 -> test2.1 -> test3.1
2055 -> test3.2
2056 -> test2.2 -> test3.3
2057
2058 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2059 run on the dependent branches in this order:
2060 test2.1, test3.1, test3.2, test2.2, test3.3
2061
2062 Note: This function does not rebase your local dependent branches. Use it when
2063 you make a change to the parent branch that will not conflict with its
2064 dependent branches, and you would like their dependencies updated in
2065 Rietveld.
2066 """
2067 if git_common.is_dirty_git_tree('upload-branch-deps'):
2068 return 1
2069
2070 root_branch = cl.GetBranch()
2071 if root_branch is None:
2072 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2073 'Get on a branch!')
2074 if not cl.GetIssue() or not cl.GetPatchset():
2075 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2076 'patchset dependencies without an uploaded CL.')
2077
2078 branches = RunGit(['for-each-ref',
2079 '--format=%(refname:short) %(upstream:short)',
2080 'refs/heads'])
2081 if not branches:
2082 print('No local branches found.')
2083 return 0
2084
2085 # Create a dictionary of all local branches to the branches that are dependent
2086 # on it.
2087 tracked_to_dependents = collections.defaultdict(list)
2088 for b in branches.splitlines():
2089 tokens = b.split()
2090 if len(tokens) == 2:
2091 branch_name, tracked = tokens
2092 tracked_to_dependents[tracked].append(branch_name)
2093
2094 print
2095 print 'The dependent local branches of %s are:' % root_branch
2096 dependents = []
2097 def traverse_dependents_preorder(branch, padding=''):
2098 dependents_to_process = tracked_to_dependents.get(branch, [])
2099 padding += ' '
2100 for dependent in dependents_to_process:
2101 print '%s%s' % (padding, dependent)
2102 dependents.append(dependent)
2103 traverse_dependents_preorder(dependent, padding)
2104 traverse_dependents_preorder(root_branch)
2105 print
2106
2107 if not dependents:
2108 print 'There are no dependent local branches for %s' % root_branch
2109 return 0
2110
2111 print ('This command will checkout all dependent branches and run '
2112 '"git cl upload".')
2113 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2114
andybons@chromium.org962f9462016-02-03 20:00:42 +00002115 # Add a default patchset title to all upload calls in Rietveld.
2116 if not settings.GetIsGerrit():
2117 args.extend(['-t', 'Updated patchset dependency'])
2118
rmistry@google.com2dd99862015-06-22 12:22:18 +00002119 # Record all dependents that failed to upload.
2120 failures = {}
2121 # Go through all dependents, checkout the branch and upload.
2122 try:
2123 for dependent_branch in dependents:
2124 print
2125 print '--------------------------------------'
2126 print 'Running "git cl upload" from %s:' % dependent_branch
2127 RunGit(['checkout', '-q', dependent_branch])
2128 print
2129 try:
2130 if CMDupload(OptionParser(), args) != 0:
2131 print 'Upload failed for %s!' % dependent_branch
2132 failures[dependent_branch] = 1
2133 except: # pylint: disable=W0702
2134 failures[dependent_branch] = 1
2135 print
2136 finally:
2137 # Swap back to the original root branch.
2138 RunGit(['checkout', '-q', root_branch])
2139
2140 print
2141 print 'Upload complete for dependent branches!'
2142 for dependent_branch in dependents:
2143 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2144 print ' %s : %s' % (dependent_branch, upload_status)
2145 print
2146
2147 return 0
2148
2149
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002151 """Show status of changelists.
2152
2153 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002154 - Red not sent for review or broken
2155 - Blue waiting for review
2156 - Yellow waiting for you to reply to review
2157 - Green LGTM'ed
2158 - Magenta in the commit queue
2159 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002160
2161 Also see 'git cl comments'.
2162 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163 parser.add_option('--field',
2164 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002165 parser.add_option('-f', '--fast', action='store_true',
2166 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002167 parser.add_option(
2168 '-j', '--maxjobs', action='store', type=int,
2169 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002170
2171 auth.add_auth_options(parser)
2172 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002173 if args:
2174 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002175 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002177 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002178 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002179 if options.field.startswith('desc'):
2180 print cl.GetDescription()
2181 elif options.field == 'id':
2182 issueid = cl.GetIssue()
2183 if issueid:
2184 print issueid
2185 elif options.field == 'patch':
2186 patchset = cl.GetPatchset()
2187 if patchset:
2188 print patchset
2189 elif options.field == 'url':
2190 url = cl.GetIssueURL()
2191 if url:
2192 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002193 return 0
2194
2195 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2196 if not branches:
2197 print('No local branch found.')
2198 return 0
2199
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002200 changes = (
2201 Changelist(branchref=b, auth_config=auth_config)
2202 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002203 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002204 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002205 alignment = max(5, max(len(b) for b in branches))
2206 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002207 output = get_cl_statuses(branches,
2208 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002209 max_processes=options.maxjobs,
2210 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002211
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002212 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002213 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002214 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002215 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002216 b, i, status = output.next()
2217 branch_statuses[b] = (i, status)
2218 issue_url, status = branch_statuses.pop(branch)
2219 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002220 reset = Fore.RESET
2221 if not sys.stdout.isatty():
2222 color = ''
2223 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002224 status_str = '(%s)' % status if status else ''
2225 print ' %*s : %s%s %s%s' % (
2226 alignment, ShortBranchName(branch), color, issue_url, status_str,
2227 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002228
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002229 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002230 print
2231 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002232 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002233 if not cl.GetIssue():
2234 print 'No issue assigned.'
2235 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002236 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002237 if not options.fast:
2238 print 'Issue description:'
2239 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002240 return 0
2241
2242
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002243def colorize_CMDstatus_doc():
2244 """To be called once in main() to add colors to git cl status help."""
2245 colors = [i for i in dir(Fore) if i[0].isupper()]
2246
2247 def colorize_line(line):
2248 for color in colors:
2249 if color in line.upper():
2250 # Extract whitespaces first and the leading '-'.
2251 indent = len(line) - len(line.lstrip(' ')) + 1
2252 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2253 return line
2254
2255 lines = CMDstatus.__doc__.splitlines()
2256 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2257
2258
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002259@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002261 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262
2263 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002264 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002265 parser.add_option('-r', '--reverse', action='store_true',
2266 help='Lookup the branch(es) for the specified issues. If '
2267 'no issues are specified, all branches with mapped '
2268 'issues will be listed.')
2269 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002270
dnj@chromium.org406c4402015-03-03 17:22:28 +00002271 if options.reverse:
2272 branches = RunGit(['for-each-ref', 'refs/heads',
2273 '--format=%(refname:short)']).splitlines()
2274
2275 # Reverse issue lookup.
2276 issue_branch_map = {}
2277 for branch in branches:
2278 cl = Changelist(branchref=branch)
2279 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2280 if not args:
2281 args = sorted(issue_branch_map.iterkeys())
2282 for issue in args:
2283 if not issue:
2284 continue
2285 print 'Branch for issue number %s: %s' % (
2286 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2287 else:
2288 cl = Changelist()
2289 if len(args) > 0:
2290 try:
2291 issue = int(args[0])
2292 except ValueError:
2293 DieWithError('Pass a number to set the issue or none to list it.\n'
2294 'Maybe you want to run git cl status?')
2295 cl.SetIssue(issue)
2296 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002297 return 0
2298
2299
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002300def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002301 """Shows or posts review comments for any changelist."""
2302 parser.add_option('-a', '--add-comment', dest='comment',
2303 help='comment to add to an issue')
2304 parser.add_option('-i', dest='issue',
2305 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002306 parser.add_option('-j', '--json-file',
2307 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002308 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002309 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002310 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002311
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002312 issue = None
2313 if options.issue:
2314 try:
2315 issue = int(options.issue)
2316 except ValueError:
2317 DieWithError('A review issue id is expected to be a number')
2318
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002319 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002320
2321 if options.comment:
2322 cl.AddComment(options.comment)
2323 return 0
2324
2325 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002326 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002327 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002328 summary.append({
2329 'date': message['date'],
2330 'lgtm': False,
2331 'message': message['text'],
2332 'not_lgtm': False,
2333 'sender': message['sender'],
2334 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002335 if message['disapproval']:
2336 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002337 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002338 elif message['approval']:
2339 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002340 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002341 elif message['sender'] == data['owner_email']:
2342 color = Fore.MAGENTA
2343 else:
2344 color = Fore.BLUE
2345 print '\n%s%s %s%s' % (
2346 color, message['date'].split('.', 1)[0], message['sender'],
2347 Fore.RESET)
2348 if message['text'].strip():
2349 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002350 if options.json_file:
2351 with open(options.json_file, 'wb') as f:
2352 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002353 return 0
2354
2355
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002356def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002357 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002358 parser.add_option('-d', '--display', action='store_true',
2359 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002360 auth.add_auth_options(parser)
2361 options, _ = parser.parse_args(args)
2362 auth_config = auth.extract_auth_config_from_options(options)
2363 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002364 if not cl.GetIssue():
2365 DieWithError('This branch has no associated changelist.')
2366 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002367 if options.display:
2368 print description.description
2369 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002370 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002371 if cl.GetDescription() != description.description:
2372 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002373 return 0
2374
2375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376def CreateDescriptionFromLog(args):
2377 """Pulls out the commit log to use as a base for the CL description."""
2378 log_args = []
2379 if len(args) == 1 and not args[0].endswith('.'):
2380 log_args = [args[0] + '..']
2381 elif len(args) == 1 and args[0].endswith('...'):
2382 log_args = [args[0][:-1]]
2383 elif len(args) == 2:
2384 log_args = [args[0] + '..' + args[1]]
2385 else:
2386 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002387 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388
2389
thestig@chromium.org44202a22014-03-11 19:22:18 +00002390def CMDlint(parser, args):
2391 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002392 parser.add_option('--filter', action='append', metavar='-x,+y',
2393 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002394 auth.add_auth_options(parser)
2395 options, args = parser.parse_args(args)
2396 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002397
2398 # Access to a protected member _XX of a client class
2399 # pylint: disable=W0212
2400 try:
2401 import cpplint
2402 import cpplint_chromium
2403 except ImportError:
2404 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2405 return 1
2406
2407 # Change the current working directory before calling lint so that it
2408 # shows the correct base.
2409 previous_cwd = os.getcwd()
2410 os.chdir(settings.GetRoot())
2411 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002412 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002413 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2414 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002415 if not files:
2416 print "Cannot lint an empty CL"
2417 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002418
2419 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002420 command = args + files
2421 if options.filter:
2422 command = ['--filter=' + ','.join(options.filter)] + command
2423 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002424
2425 white_regex = re.compile(settings.GetLintRegex())
2426 black_regex = re.compile(settings.GetLintIgnoreRegex())
2427 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2428 for filename in filenames:
2429 if white_regex.match(filename):
2430 if black_regex.match(filename):
2431 print "Ignoring file %s" % filename
2432 else:
2433 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2434 extra_check_functions)
2435 else:
2436 print "Skipping file %s" % filename
2437 finally:
2438 os.chdir(previous_cwd)
2439 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2440 if cpplint._cpplint_state.error_count != 0:
2441 return 1
2442 return 0
2443
2444
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002446 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002447 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002448 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002449 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002450 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002451 auth.add_auth_options(parser)
2452 options, args = parser.parse_args(args)
2453 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002454
sbc@chromium.org71437c02015-04-09 19:29:40 +00002455 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002456 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002457 return 1
2458
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002459 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002460 if args:
2461 base_branch = args[0]
2462 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002463 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002464 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002465
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002466 cl.RunHook(
2467 committing=not options.upload,
2468 may_prompt=False,
2469 verbose=options.verbose,
2470 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002471 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472
2473
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002474def AddChangeIdToCommitMessage(options, args):
2475 """Re-commits using the current message, assumes the commit hook is in
2476 place.
2477 """
2478 log_desc = options.message or CreateDescriptionFromLog(args)
2479 git_command = ['commit', '--amend', '-m', log_desc]
2480 RunGit(git_command)
2481 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002482 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002483 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002484 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002485 else:
2486 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2487
2488
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002489def GenerateGerritChangeId(message):
2490 """Returns Ixxxxxx...xxx change id.
2491
2492 Works the same way as
2493 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2494 but can be called on demand on all platforms.
2495
2496 The basic idea is to generate git hash of a state of the tree, original commit
2497 message, author/committer info and timestamps.
2498 """
2499 lines = []
2500 tree_hash = RunGitSilent(['write-tree'])
2501 lines.append('tree %s' % tree_hash.strip())
2502 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2503 if code == 0:
2504 lines.append('parent %s' % parent.strip())
2505 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2506 lines.append('author %s' % author.strip())
2507 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2508 lines.append('committer %s' % committer.strip())
2509 lines.append('')
2510 # Note: Gerrit's commit-hook actually cleans message of some lines and
2511 # whitespace. This code is not doing this, but it clearly won't decrease
2512 # entropy.
2513 lines.append(message)
2514 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2515 stdin='\n'.join(lines))
2516 return 'I%s' % change_hash.strip()
2517
2518
piman@chromium.org336f9122014-09-04 02:16:55 +00002519def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002520 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002521 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2522 # to avoid private members accessors below.
2523
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002524 # We assume the remote called "origin" is the one we want.
2525 # It is probably not worthwhile to support different workflows.
2526 gerrit_remote = 'origin'
2527
luqui@chromium.org609f3952015-05-04 22:47:04 +00002528 remote, remote_branch = cl.GetRemoteBranch()
2529 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2530 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002531
andybons@chromium.org962f9462016-02-03 20:00:42 +00002532 if options.title:
2533 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002534 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002535
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002536 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002537 if not cl.GetIssue():
2538 # TODO(tandrii): deperecate this after 2016Q2.
2539 # Backwards compatibility with shadow branch, which used to contain
2540 # change-id for a given branch, using which we can fetch actual issue
2541 # number and set it as the property of the branch, which is the new way.
2542 message = RunGitSilent(['show', '--format=%B', '-s',
2543 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2544 if message:
2545 change_ids = git_footers.get_footer_change_id(message.strip())
2546 if change_ids and len(change_ids) == 1:
2547 details = gerrit_util.GetChangeDetail(
2548 cl._codereview_impl._GetGerritHost(), change_ids[0])
2549 if details:
2550 print('WARNING: found old upload in branch git_cl_uploads/%s '
2551 'corresponding to issue %s' %
2552 (cl.GetBranch(), details['_number']))
2553 cl.SetIssue(details['_number'])
2554 if not cl.GetIssue():
2555 DieWithError(
2556 '\n' # For readability of the blob below.
2557 'Found old upload in branch git_cl_uploads/%s, '
2558 'but failed to find corresponding Gerrit issue.\n'
2559 'If you know the issue number, set it manually first:\n'
2560 ' git cl issue 123456\n'
2561 'If you intended to upload this CL as new issue, '
2562 'just delete or rename the old upload branch:\n'
2563 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2564 'After that, please run git cl upload again.' %
2565 tuple([cl.GetBranch()] * 3))
2566 # End of backwards compatability.
2567
2568 if cl.GetIssue():
2569 # Try to get the message from a previous upload.
2570 message = cl.GetDescription()
2571 if not message:
2572 DieWithError(
2573 'failed to fetch description from current Gerrit issue %d\n'
2574 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2575 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2576 while True:
2577 footer_change_ids = git_footers.get_footer_change_id(message)
2578 if footer_change_ids == [change_id]:
2579 break
2580 if not footer_change_ids:
2581 message = git_footers.add_footer_change_id(message, change_id)
2582 print('WARNING: appended missing Change-Id to issue description')
2583 continue
2584 # There is already a valid footer but with different or several ids.
2585 # Doing this automatically is non-trivial as we don't want to lose
2586 # existing other footers, yet we want to append just 1 desired
2587 # Change-Id. Thus, just create a new footer, but let user verify the new
2588 # description.
2589 message = '%s\n\nChange-Id: %s' % (message, change_id)
2590 print(
2591 'WARNING: issue %s has Change-Id footer(s):\n'
2592 ' %s\n'
2593 'but issue has Change-Id %s, according to Gerrit.\n'
2594 'Please, check the proposed correction to the description, '
2595 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2596 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2597 change_id))
2598 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2599 if not options.force:
2600 change_desc = ChangeDescription(message)
2601 change_desc.prompt()
2602 message = change_desc.description
2603 if not message:
2604 DieWithError("Description is empty. Aborting...")
2605 # Continue the while loop.
2606 # Sanity check of this code - we should end up with proper message footer.
2607 assert [change_id] == git_footers.get_footer_change_id(message)
2608 change_desc = ChangeDescription(message)
2609 else:
2610 change_desc = ChangeDescription(
2611 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002612 if not options.force:
2613 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002614 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002615 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002616 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002617 change_ids = git_footers.get_footer_change_id(message)
2618 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002619 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002620 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002621 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002622 message = git_footers.add_footer_change_id(
2623 message, GenerateGerritChangeId(message))
2624 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002625 change_ids = git_footers.get_footer_change_id(message)
2626 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002627 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002628
2629 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2630 if remote is '.':
2631 # If our upstream branch is local, we base our squashed commit on its
2632 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002633 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2634 # Check the squashed hash of the parent.
2635 parent = RunGit(['config',
2636 'branch.%s.gerritsquashhash' % upstream_branch_name],
2637 error_ok=True).strip()
2638 # Verify that the upstream branch has been uploaded too, otherwise
2639 # Gerrit will create additional CLs when uploading.
2640 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2641 RunGitSilent(['rev-parse', parent + ':'])):
2642 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2643 DieWithError(
2644 'Upload upstream branch %s first.\n'
2645 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2646 ' version of depot_tools. If so, then re-upload it with:\n'
2647 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002648 else:
2649 parent = cl.GetCommonAncestorWithUpstream()
2650
2651 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2652 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2653 '-m', message]).strip()
2654 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002655 change_desc = ChangeDescription(
2656 options.message or CreateDescriptionFromLog(args))
2657 if not change_desc.description:
2658 DieWithError("Description is empty. Aborting...")
2659
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002660 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002661 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002662 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002663 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002664 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002665 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002666
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002667 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002668 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2669 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002670 if len(commits) > 1:
2671 print('WARNING: This will upload %d commits. Run the following command '
2672 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002673 print('git log %s..%s' % (parent, ref_to_push))
2674 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002675 'commit.')
2676 ask_for_data('About to upload; enter to confirm.')
2677
piman@chromium.org336f9122014-09-04 02:16:55 +00002678 if options.reviewers or options.tbr_owners:
2679 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002680
ukai@chromium.orge8077812012-02-03 03:41:46 +00002681 receive_options = []
2682 cc = cl.GetCCList().split(',')
2683 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002684 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002685 cc = filter(None, cc)
2686 if cc:
2687 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002688 if change_desc.get_reviewers():
2689 receive_options.extend(
2690 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691
ukai@chromium.orge8077812012-02-03 03:41:46 +00002692 git_command = ['push']
2693 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002694 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002695 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002696 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002697 push_stdout = gclient_utils.CheckCallAndFilter(
2698 ['git'] + git_command,
2699 print_stdout=True,
2700 # Flush after every line: useful for seeing progress when running as
2701 # recipe.
2702 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002703
2704 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002705 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2706 change_numbers = [m.group(1)
2707 for m in map(regex.match, push_stdout.splitlines())
2708 if m]
2709 if len(change_numbers) != 1:
2710 DieWithError(
2711 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2712 'Change-Id: %s') % (len(change_numbers), change_id))
2713 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002714 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2715 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002716 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718
wittman@chromium.org455dc922015-01-26 20:15:50 +00002719def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2720 """Computes the remote branch ref to use for the CL.
2721
2722 Args:
2723 remote (str): The git remote for the CL.
2724 remote_branch (str): The git remote branch for the CL.
2725 target_branch (str): The target branch specified by the user.
2726 pending_prefix (str): The pending prefix from the settings.
2727 """
2728 if not (remote and remote_branch):
2729 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002730
wittman@chromium.org455dc922015-01-26 20:15:50 +00002731 if target_branch:
2732 # Cannonicalize branch references to the equivalent local full symbolic
2733 # refs, which are then translated into the remote full symbolic refs
2734 # below.
2735 if '/' not in target_branch:
2736 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2737 else:
2738 prefix_replacements = (
2739 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2740 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2741 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2742 )
2743 match = None
2744 for regex, replacement in prefix_replacements:
2745 match = re.search(regex, target_branch)
2746 if match:
2747 remote_branch = target_branch.replace(match.group(0), replacement)
2748 break
2749 if not match:
2750 # This is a branch path but not one we recognize; use as-is.
2751 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002752 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2753 # Handle the refs that need to land in different refs.
2754 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002755
wittman@chromium.org455dc922015-01-26 20:15:50 +00002756 # Create the true path to the remote branch.
2757 # Does the following translation:
2758 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2759 # * refs/remotes/origin/master -> refs/heads/master
2760 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2761 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2762 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2763 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2764 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2765 'refs/heads/')
2766 elif remote_branch.startswith('refs/remotes/branch-heads'):
2767 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2768 # If a pending prefix exists then replace refs/ with it.
2769 if pending_prefix:
2770 remote_branch = remote_branch.replace('refs/', pending_prefix)
2771 return remote_branch
2772
2773
piman@chromium.org336f9122014-09-04 02:16:55 +00002774def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002775 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002777 upload_args.extend(['--server', cl.GetCodereviewServer()])
2778 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2779 upload_args.extend(auth.auth_config_to_command_options(
2780 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002781 if options.emulate_svn_auto_props:
2782 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002783
2784 change_desc = None
2785
pgervais@chromium.org91141372014-01-09 23:27:20 +00002786 if options.email is not None:
2787 upload_args.extend(['--email', options.email])
2788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002789 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002790 if options.title:
2791 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002792 if options.message:
2793 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002794 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795 print ("This branch is associated with issue %s. "
2796 "Adding patch to that issue." % cl.GetIssue())
2797 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002798 if options.title:
2799 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002800 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002801 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002802 if options.reviewers or options.tbr_owners:
2803 change_desc.update_reviewers(options.reviewers,
2804 options.tbr_owners,
2805 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002806 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002807 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002808
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002809 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810 print "Description is empty; aborting."
2811 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002812
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002813 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002814 if change_desc.get_reviewers():
2815 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002816 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002818 DieWithError("Must specify reviewers to send email.")
2819 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002820
2821 # We check this before applying rietveld.private assuming that in
2822 # rietveld.cc only addresses which we can send private CLs to are listed
2823 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2824 # --private is specified explicitly on the command line.
2825 if options.private:
2826 logging.warn('rietveld.cc is ignored since private flag is specified. '
2827 'You need to review and add them manually if necessary.')
2828 cc = cl.GetCCListWithoutDefault()
2829 else:
2830 cc = cl.GetCCList()
2831 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002832 if cc:
2833 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002834
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002835 if options.private or settings.GetDefaultPrivateFlag() == "True":
2836 upload_args.append('--private')
2837
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002838 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002839 if not options.find_copies:
2840 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842 # Include the upstream repo's URL in the change -- this is useful for
2843 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002844 remote_url = cl.GetGitBaseUrlFromConfig()
2845 if not remote_url:
2846 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002847 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002848 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002849 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2850 remote_url = (cl.GetRemoteUrl() + '@'
2851 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852 if remote_url:
2853 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002854 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002855 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2856 settings.GetPendingRefPrefix())
2857 if target_ref:
2858 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002860 # Look for dependent patchsets. See crbug.com/480453 for more details.
2861 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2862 upstream_branch = ShortBranchName(upstream_branch)
2863 if remote is '.':
2864 # A local branch is being tracked.
2865 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002866 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002867 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002868 print ('Skipping dependency patchset upload because git config '
2869 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002870 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002871 else:
2872 auth_config = auth.extract_auth_config_from_options(options)
2873 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2874 branch_cl_issue_url = branch_cl.GetIssueURL()
2875 branch_cl_issue = branch_cl.GetIssue()
2876 branch_cl_patchset = branch_cl.GetPatchset()
2877 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2878 upload_args.extend(
2879 ['--depends_on_patchset', '%s:%s' % (
2880 branch_cl_issue, branch_cl_patchset)])
2881 print
2882 print ('The current branch (%s) is tracking a local branch (%s) with '
2883 'an associated CL.') % (cl.GetBranch(), local_branch)
2884 print 'Adding %s/#ps%s as a dependency patchset.' % (
2885 branch_cl_issue_url, branch_cl_patchset)
2886 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002887
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002888 project = settings.GetProject()
2889 if project:
2890 upload_args.extend(['--project', project])
2891
rmistry@google.comef966222015-04-07 11:15:01 +00002892 if options.cq_dry_run:
2893 upload_args.extend(['--cq_dry_run'])
2894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002896 upload_args = ['upload'] + upload_args + args
2897 logging.info('upload.RealMain(%s)', upload_args)
2898 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002899 issue = int(issue)
2900 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002901 except KeyboardInterrupt:
2902 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903 except:
2904 # If we got an exception after the user typed a description for their
2905 # change, back up the description before re-raising.
2906 if change_desc:
2907 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2908 print '\nGot exception while uploading -- saving description to %s\n' \
2909 % backup_path
2910 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002911 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912 backup_file.close()
2913 raise
2914
2915 if not cl.GetIssue():
2916 cl.SetIssue(issue)
2917 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002918
2919 if options.use_commit_queue:
2920 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 return 0
2922
2923
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002924def cleanup_list(l):
2925 """Fixes a list so that comma separated items are put as individual items.
2926
2927 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2928 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2929 """
2930 items = sum((i.split(',') for i in l), [])
2931 stripped_items = (i.strip() for i in items)
2932 return sorted(filter(None, stripped_items))
2933
2934
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002935@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002936def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002937 """Uploads the current changelist to codereview.
2938
2939 Can skip dependency patchset uploads for a branch by running:
2940 git config branch.branch_name.skip-deps-uploads True
2941 To unset run:
2942 git config --unset branch.branch_name.skip-deps-uploads
2943 Can also set the above globally by using the --global flag.
2944 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002945 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2946 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002947 parser.add_option('--bypass-watchlists', action='store_true',
2948 dest='bypass_watchlists',
2949 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002950 parser.add_option('-f', action='store_true', dest='force',
2951 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002952 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002953 parser.add_option('-t', dest='title',
2954 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002955 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002956 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002957 help='reviewer email addresses')
2958 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002959 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002960 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002961 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002962 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002963 parser.add_option('--emulate_svn_auto_props',
2964 '--emulate-svn-auto-props',
2965 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002966 dest="emulate_svn_auto_props",
2967 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002968 parser.add_option('-c', '--use-commit-queue', action='store_true',
2969 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002970 parser.add_option('--private', action='store_true',
2971 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002972 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002973 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002974 metavar='TARGET',
2975 help='Apply CL to remote ref TARGET. ' +
2976 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002977 parser.add_option('--squash', action='store_true',
2978 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002979 parser.add_option('--no-squash', action='store_true',
2980 help='Don\'t squash multiple commits into one ' +
2981 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002982 parser.add_option('--email', default=None,
2983 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002984 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2985 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002986 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2987 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002988 help='Send the patchset to do a CQ dry run right after '
2989 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002990 parser.add_option('--dependencies', action='store_true',
2991 help='Uploads CLs of all the local branches that depend on '
2992 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002993
rmistry@google.com2dd99862015-06-22 12:22:18 +00002994 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002995 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002996 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002997 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002998 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002999
sbc@chromium.org71437c02015-04-09 19:29:40 +00003000 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003001 return 1
3002
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003003 options.reviewers = cleanup_list(options.reviewers)
3004 options.cc = cleanup_list(options.cc)
3005
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003006 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3007 settings.GetIsGerrit()
3008
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003009 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003010 if args:
3011 # TODO(ukai): is it ok for gerrit case?
3012 base_branch = args[0]
3013 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003014 if cl.GetBranch() is None:
3015 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3016
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003017 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003018 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003019 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003020
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003021 # Make sure authenticated to Rietveld before running expensive hooks. It is
3022 # a fast, best efforts check. Rietveld still can reject the authentication
3023 # during the actual upload.
3024 if not settings.GetIsGerrit() and auth_config.use_oauth2:
3025 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003026 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003027 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003028 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003029
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003030 # Apply watchlists on upload.
3031 change = cl.GetChange(base_branch, None)
3032 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3033 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003034 if not options.bypass_watchlists:
3035 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003036
ukai@chromium.orge8077812012-02-03 03:41:46 +00003037 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003038 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003039 # Set the reviewer list now so that presubmit checks can access it.
3040 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003041 change_description.update_reviewers(options.reviewers,
3042 options.tbr_owners,
3043 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003044 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003045 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003046 may_prompt=not options.force,
3047 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003048 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003049 if not hook_results.should_continue():
3050 return 1
3051 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003052 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003053
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003054 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003055 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003056 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003057 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003058 print ('The last upload made from this repository was patchset #%d but '
3059 'the most recent patchset on the server is #%d.'
3060 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003061 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3062 'from another machine or branch the patch you\'re uploading now '
3063 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003064 ask_for_data('About to upload; enter to confirm.')
3065
iannucci@chromium.org79540052012-10-19 23:15:26 +00003066 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003067 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003068 if options.squash and options.no_squash:
3069 DieWithError('Can only use one of --squash or --no-squash')
3070
3071 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3072 not options.no_squash)
3073
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003074 ret = GerritUpload(options, args, cl, change)
3075 else:
3076 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003077 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003078 git_set_branch_value('last-upload-hash',
3079 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003080 # Run post upload hooks, if specified.
3081 if settings.GetRunPostUploadHook():
3082 presubmit_support.DoPostUploadExecuter(
3083 change,
3084 cl,
3085 settings.GetRoot(),
3086 options.verbose,
3087 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003088
rmistry@google.com2dd99862015-06-22 12:22:18 +00003089 # Upload all dependencies if specified.
3090 if options.dependencies:
3091 print
3092 print '--dependencies has been specified.'
3093 print 'All dependent local branches will be re-uploaded.'
3094 print
3095 # Remove the dependencies flag from args so that we do not end up in a
3096 # loop.
3097 orig_args.remove('--dependencies')
3098 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003099 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003100
3101
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003102def IsSubmoduleMergeCommit(ref):
3103 # When submodules are added to the repo, we expect there to be a single
3104 # non-git-svn merge commit at remote HEAD with a signature comment.
3105 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003106 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003107 return RunGit(cmd) != ''
3108
3109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003111 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003113 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114 Updates changelog with metadata (e.g. pointer to review).
3115 Pushes/dcommits the code upstream.
3116 Updates review and closes.
3117 """
3118 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3119 help='bypass upload presubmit hook')
3120 parser.add_option('-m', dest='message',
3121 help="override review description")
3122 parser.add_option('-f', action='store_true', dest='force',
3123 help="force yes to questions (don't prompt)")
3124 parser.add_option('-c', dest='contributor',
3125 help="external contributor for patch (appended to " +
3126 "description and used as author for git). Should be " +
3127 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003128 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003129 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003130 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003131 auth_config = auth.extract_auth_config_from_options(options)
3132
3133 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003134
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003135 current = cl.GetBranch()
3136 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3137 if not settings.GetIsGitSvn() and remote == '.':
3138 print
3139 print 'Attempting to push branch %r into another local branch!' % current
3140 print
3141 print 'Either reparent this branch on top of origin/master:'
3142 print ' git reparent-branch --root'
3143 print
3144 print 'OR run `git rebase-update` if you think the parent branch is already'
3145 print 'committed.'
3146 print
3147 print ' Current parent: %r' % upstream_branch
3148 return 1
3149
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003150 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003151 # Default to merging against our best guess of the upstream branch.
3152 args = [cl.GetUpstreamBranch()]
3153
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003154 if options.contributor:
3155 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3156 print "Please provide contibutor as 'First Last <email@example.com>'"
3157 return 1
3158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003160 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003161
sbc@chromium.org71437c02015-04-09 19:29:40 +00003162 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003163 return 1
3164
3165 # This rev-list syntax means "show all commits not in my branch that
3166 # are in base_branch".
3167 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3168 base_branch]).splitlines()
3169 if upstream_commits:
3170 print ('Base branch "%s" has %d commits '
3171 'not in this branch.' % (base_branch, len(upstream_commits)))
3172 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3173 return 1
3174
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003175 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003176 svn_head = None
3177 if cmd == 'dcommit' or base_has_submodules:
3178 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3179 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003182 # If the base_head is a submodule merge commit, the first parent of the
3183 # base_head should be a git-svn commit, which is what we're interested in.
3184 base_svn_head = base_branch
3185 if base_has_submodules:
3186 base_svn_head += '^1'
3187
3188 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189 if extra_commits:
3190 print ('This branch has %d additional commits not upstreamed yet.'
3191 % len(extra_commits.splitlines()))
3192 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3193 'before attempting to %s.' % (base_branch, cmd))
3194 return 1
3195
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003196 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003197 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003198 author = None
3199 if options.contributor:
3200 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003201 hook_results = cl.RunHook(
3202 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003203 may_prompt=not options.force,
3204 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003205 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003206 if not hook_results.should_continue():
3207 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003209 # Check the tree status if the tree status URL is set.
3210 status = GetTreeStatus()
3211 if 'closed' == status:
3212 print('The tree is closed. Please wait for it to reopen. Use '
3213 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3214 return 1
3215 elif 'unknown' == status:
3216 print('Unable to determine tree status. Please verify manually and '
3217 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3218 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003220 change_desc = ChangeDescription(options.message)
3221 if not change_desc.description and cl.GetIssue():
3222 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003224 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003225 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003226 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003227 else:
3228 print 'No description set.'
3229 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3230 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003232 # Keep a separate copy for the commit message, because the commit message
3233 # contains the link to the Rietveld issue, while the Rietveld message contains
3234 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003235 # Keep a separate copy for the commit message.
3236 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003237 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003238
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003239 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003240 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003241 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003242 # after it. Add a period on a new line to circumvent this. Also add a space
3243 # before the period to make sure that Gitiles continues to correctly resolve
3244 # the URL.
3245 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003247 commit_desc.append_footer('Patch from %s.' % options.contributor)
3248
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003249 print('Description:')
3250 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003252 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003254 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003256 # We want to squash all this branch's commits into one commit with the proper
3257 # description. We do this by doing a "reset --soft" to the base branch (which
3258 # keeps the working copy the same), then dcommitting that. If origin/master
3259 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3260 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003262 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3263 # Delete the branches if they exist.
3264 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3265 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3266 result = RunGitWithCode(showref_cmd)
3267 if result[0] == 0:
3268 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269
3270 # We might be in a directory that's present in this branch but not in the
3271 # trunk. Move up to the top of the tree so that git commands that expect a
3272 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003273 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003274 if rel_base_path:
3275 os.chdir(rel_base_path)
3276
3277 # Stuff our change into the merge branch.
3278 # We wrap in a try...finally block so if anything goes wrong,
3279 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003280 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003281 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003282 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003283 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003285 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003286 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003287 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003288 RunGit(
3289 [
3290 'commit', '--author', options.contributor,
3291 '-m', commit_desc.description,
3292 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003293 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003294 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003295 if base_has_submodules:
3296 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3297 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3298 RunGit(['checkout', CHERRY_PICK_BRANCH])
3299 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003300 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003301 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003302 mirror = settings.GetGitMirror(remote)
3303 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003304 pending_prefix = settings.GetPendingRefPrefix()
3305 if not pending_prefix or branch.startswith(pending_prefix):
3306 # If not using refs/pending/heads/* at all, or target ref is already set
3307 # to pending, then push to the target ref directly.
3308 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003309 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003310 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003311 else:
3312 # Cherry-pick the change on top of pending ref and then push it.
3313 assert branch.startswith('refs/'), branch
3314 assert pending_prefix[-1] == '/', pending_prefix
3315 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003316 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003317 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003318 if retcode == 0:
3319 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003320 else:
3321 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003322 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003323 'svn', 'dcommit',
3324 '-C%s' % options.similarity,
3325 '--no-rebase', '--rmdir',
3326 ]
3327 if settings.GetForceHttpsCommitUrl():
3328 # Allow forcing https commit URLs for some projects that don't allow
3329 # committing to http URLs (like Google Code).
3330 remote_url = cl.GetGitSvnRemoteUrl()
3331 if urlparse.urlparse(remote_url).scheme == 'http':
3332 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003333 cmd_args.append('--commit-url=%s' % remote_url)
3334 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003335 if 'Committed r' in output:
3336 revision = re.match(
3337 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3338 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339 finally:
3340 # And then swap back to the original branch and clean up.
3341 RunGit(['checkout', '-q', cl.GetBranch()])
3342 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003343 if base_has_submodules:
3344 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003346 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003347 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003348 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003349
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003350 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003351 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003352 try:
3353 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3354 # We set pushed_to_pending to False, since it made it all the way to the
3355 # real ref.
3356 pushed_to_pending = False
3357 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003358 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003359
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003360 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003361 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003363 if not to_pending:
3364 if viewvc_url and revision:
3365 change_desc.append_footer(
3366 'Committed: %s%s' % (viewvc_url, revision))
3367 elif revision:
3368 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369 print ('Closing issue '
3370 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003371 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003372 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003373 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003374 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003375 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003376 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003377 if options.bypass_hooks:
3378 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3379 else:
3380 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003381 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003382 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003383
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003384 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003385 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3386 print 'The commit is in the pending queue (%s).' % pending_ref
3387 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003388 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003389 'footer.' % branch)
3390
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003391 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3392 if os.path.isfile(hook):
3393 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003394
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003395 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003396
3397
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003398def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3399 print
3400 print 'Waiting for commit to be landed on %s...' % real_ref
3401 print '(If you are impatient, you may Ctrl-C once without harm)'
3402 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3403 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003404 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003405
3406 loop = 0
3407 while True:
3408 sys.stdout.write('fetching (%d)... \r' % loop)
3409 sys.stdout.flush()
3410 loop += 1
3411
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003412 if mirror:
3413 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003414 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3415 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3416 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3417 for commit in commits.splitlines():
3418 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3419 print 'Found commit on %s' % real_ref
3420 return commit
3421
3422 current_rev = to_rev
3423
3424
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003425def PushToGitPending(remote, pending_ref, upstream_ref):
3426 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3427
3428 Returns:
3429 (retcode of last operation, output log of last operation).
3430 """
3431 assert pending_ref.startswith('refs/'), pending_ref
3432 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3433 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3434 code = 0
3435 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003436 max_attempts = 3
3437 attempts_left = max_attempts
3438 while attempts_left:
3439 if attempts_left != max_attempts:
3440 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3441 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003442
3443 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003444 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003445 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003446 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003447 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003448 print 'Fetch failed with exit code %d.' % code
3449 if out.strip():
3450 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003451 continue
3452
3453 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003454 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003455 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003456 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003457 if code:
3458 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003459 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3460 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003461 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3462 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003463 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003464 return code, out
3465
3466 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003467 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003468 code, out = RunGitWithCode(
3469 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3470 if code == 0:
3471 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003472 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003473 return code, out
3474
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003475 print 'Push failed with exit code %d.' % code
3476 if out.strip():
3477 print out.strip()
3478 if IsFatalPushFailure(out):
3479 print (
3480 'Fatal push error. Make sure your .netrc credentials and git '
3481 'user.email are correct and you have push access to the repo.')
3482 return code, out
3483
3484 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003485 return code, out
3486
3487
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003488def IsFatalPushFailure(push_stdout):
3489 """True if retrying push won't help."""
3490 return '(prohibited by Gerrit)' in push_stdout
3491
3492
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003493@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003494def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003495 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003497 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003498 # If it looks like previous commits were mirrored with git-svn.
3499 message = """This repository appears to be a git-svn mirror, but no
3500upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3501 else:
3502 message = """This doesn't appear to be an SVN repository.
3503If your project has a true, writeable git repository, you probably want to run
3504'git cl land' instead.
3505If your project has a git mirror of an upstream SVN master, you probably need
3506to run 'git svn init'.
3507
3508Using the wrong command might cause your commit to appear to succeed, and the
3509review to be closed, without actually landing upstream. If you choose to
3510proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003511 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003512 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003513 return SendUpstream(parser, args, 'dcommit')
3514
3515
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003516@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003517def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003518 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003519 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520 print('This appears to be an SVN repository.')
3521 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003522 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003523 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003524 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525
3526
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003527def ParseIssueNum(arg):
3528 """Parses the issue number from args if present otherwise returns None."""
3529 if re.match(r'\d+', arg):
3530 return arg
3531 if arg.startswith('http'):
3532 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3533 return None
3534
3535
3536@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003538 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539 parser.add_option('-b', dest='newbranch',
3540 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003541 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003543 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3544 help='Change to the directory DIR immediately, '
3545 'before doing anything else.')
3546 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003547 help='failed patches spew .rej files rather than '
3548 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3550 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003551
3552 group = optparse.OptionGroup(parser,
3553 """Options for continuing work on the current issue uploaded
3554from a different clone (e.g. different machine). Must be used independently from
3555the other options. No issue number should be specified, and the branch must have
3556an issue number associated with it""")
3557 group.add_option('--reapply', action='store_true',
3558 dest='reapply',
3559 help="""Reset the branch and reapply the issue.
3560CAUTION: This will undo any local changes in this branch""")
3561
3562 group.add_option('--pull', action='store_true', dest='pull',
3563 help="Performs a pull before reapplying.")
3564 parser.add_option_group(group)
3565
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003566 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003567 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003568 auth_config = auth.extract_auth_config_from_options(options)
3569
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003570 issue_arg = None
3571 if options.reapply :
3572 if len(args) > 0:
3573 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003574
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003575 cl = Changelist()
3576 issue_arg = cl.GetIssue()
3577 upstream = cl.GetUpstreamBranch()
3578 if upstream == None:
3579 parser.error("No upstream branch specified. Cannot reset branch")
3580
3581 RunGit(['reset', '--hard', upstream])
3582 if options.pull:
3583 RunGit(['pull'])
3584 else:
3585 if len(args) != 1:
3586 parser.error("Must specify issue number")
3587
3588 issue_arg = ParseIssueNum(args[0])
3589
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003590 # The patch URL works because ParseIssueNum won't do any substitution
3591 # as the re.sub pattern fails to match and just returns it.
3592 if issue_arg == None:
3593 parser.print_help()
3594 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003596 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003597 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003598 return 1
3599
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003600 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003601 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003602
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003603 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003604 if options.reapply:
3605 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003606 if options.force:
3607 RunGit(['branch', '-D', options.newbranch],
3608 stderr=subprocess2.PIPE, error_ok=True)
3609 RunGit(['checkout', '-b', options.newbranch,
3610 Changelist().GetUpstreamBranch()])
3611
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003612 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003613 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003614
3615
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003616def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003617 # PatchIssue should never be called with a dirty tree. It is up to the
3618 # caller to check this, but just in case we assert here since the
3619 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003620 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003621
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003622 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003623 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003625 issue = int(issue_arg)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003626 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003627 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003628 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003630 # Assume it's a URL to the patch. Default to https.
3631 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003632 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003633 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634 DieWithError('Must pass an issue ID or full URL for '
3635 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003636 issue = int(match.group(2))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003637 cl = Changelist(issue=issue, codereview='rietveld',
3638 rietvled_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003639 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003640 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642 # Switch up to the top-level directory, if necessary, in preparation for
3643 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003644 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645 if top:
3646 os.chdir(top)
3647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648 # Git patches have a/ at the beginning of source paths. We strip that out
3649 # with a sed script rather than the -p flag to patch so we can feed either
3650 # Git or svn-style patches into the same apply command.
3651 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003652 try:
3653 patch_data = subprocess2.check_output(
3654 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3655 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 DieWithError('Git patch mungling failed.')
3657 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659 # We use "git apply" to apply the patch instead of "patch" so that we can
3660 # pick up file adds.
3661 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003662 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003663 if directory:
3664 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003665 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003666 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003667 elif IsGitVersionAtLeast('1.7.12'):
3668 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003669 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003670 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003671 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003672 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003673 print 'Failed to apply the patch'
3674 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675
3676 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003677 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003678 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3679 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003680 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3681 % {'i': issue, 'p': patchset})])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003682 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3683 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003685 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003686 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687 else:
3688 print "Patch applied to index."
3689 return 0
3690
3691
3692def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003693 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003694 # Provide a wrapper for git svn rebase to help avoid accidental
3695 # git svn dcommit.
3696 # It's the only command that doesn't use parser at all since we just defer
3697 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003698
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003699 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003700
3701
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003702def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 """Fetches the tree status and returns either 'open', 'closed',
3704 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003705 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706 if url:
3707 status = urllib2.urlopen(url).read().lower()
3708 if status.find('closed') != -1 or status == '0':
3709 return 'closed'
3710 elif status.find('open') != -1 or status == '1':
3711 return 'open'
3712 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713 return 'unset'
3714
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003715
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003716def GetTreeStatusReason():
3717 """Fetches the tree status from a json url and returns the message
3718 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003719 url = settings.GetTreeStatusUrl()
3720 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003721 connection = urllib2.urlopen(json_url)
3722 status = json.loads(connection.read())
3723 connection.close()
3724 return status['message']
3725
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003726
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003727def GetBuilderMaster(bot_list):
3728 """For a given builder, fetch the master from AE if available."""
3729 map_url = 'https://builders-map.appspot.com/'
3730 try:
3731 master_map = json.load(urllib2.urlopen(map_url))
3732 except urllib2.URLError as e:
3733 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3734 (map_url, e))
3735 except ValueError as e:
3736 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3737 if not master_map:
3738 return None, 'Failed to build master map.'
3739
3740 result_master = ''
3741 for bot in bot_list:
3742 builder = bot.split(':', 1)[0]
3743 master_list = master_map.get(builder, [])
3744 if not master_list:
3745 return None, ('No matching master for builder %s.' % builder)
3746 elif len(master_list) > 1:
3747 return None, ('The builder name %s exists in multiple masters %s.' %
3748 (builder, master_list))
3749 else:
3750 cur_master = master_list[0]
3751 if not result_master:
3752 result_master = cur_master
3753 elif result_master != cur_master:
3754 return None, 'The builders do not belong to the same master.'
3755 return result_master, None
3756
3757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003759 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003760 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761 status = GetTreeStatus()
3762 if 'unset' == status:
3763 print 'You must configure your tree status URL by running "git cl config".'
3764 return 2
3765
3766 print "The tree is %s" % status
3767 print
3768 print GetTreeStatusReason()
3769 if status != 'open':
3770 return 1
3771 return 0
3772
3773
maruel@chromium.org15192402012-09-06 12:38:29 +00003774def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003775 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003776 group = optparse.OptionGroup(parser, "Try job options")
3777 group.add_option(
3778 "-b", "--bot", action="append",
3779 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3780 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003781 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003782 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003783 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003784 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003785 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003786 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003787 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003788 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003789 "-r", "--revision",
3790 help="Revision to use for the try job; default: the "
3791 "revision will be determined by the try server; see "
3792 "its waterfall for more info")
3793 group.add_option(
3794 "-c", "--clobber", action="store_true", default=False,
3795 help="Force a clobber before building; e.g. don't do an "
3796 "incremental build")
3797 group.add_option(
3798 "--project",
3799 help="Override which project to use. Projects are defined "
3800 "server-side to define what default bot set to use")
3801 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003802 "-p", "--property", dest="properties", action="append", default=[],
3803 help="Specify generic properties in the form -p key1=value1 -p "
3804 "key2=value2 etc (buildbucket only). The value will be treated as "
3805 "json if decodable, or as string otherwise.")
3806 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003807 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003808 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003809 "--use-rietveld", action="store_true", default=False,
3810 help="Use Rietveld to trigger try jobs.")
3811 group.add_option(
3812 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3813 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003814 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003815 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003816 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003817 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003818
machenbach@chromium.org45453142015-09-15 08:45:22 +00003819 if options.use_rietveld and options.properties:
3820 parser.error('Properties can only be specified with buildbucket')
3821
3822 # Make sure that all properties are prop=value pairs.
3823 bad_params = [x for x in options.properties if '=' not in x]
3824 if bad_params:
3825 parser.error('Got properties with missing "=": %s' % bad_params)
3826
maruel@chromium.org15192402012-09-06 12:38:29 +00003827 if args:
3828 parser.error('Unknown arguments: %s' % args)
3829
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003830 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003831 if not cl.GetIssue():
3832 parser.error('Need to upload first')
3833
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003834 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003835 if props.get('closed'):
3836 parser.error('Cannot send tryjobs for a closed CL')
3837
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003838 if props.get('private'):
3839 parser.error('Cannot use trybots with private issue')
3840
maruel@chromium.org15192402012-09-06 12:38:29 +00003841 if not options.name:
3842 options.name = cl.GetBranch()
3843
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003844 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003845 options.master, err_msg = GetBuilderMaster(options.bot)
3846 if err_msg:
3847 parser.error('Tryserver master cannot be found because: %s\n'
3848 'Please manually specify the tryserver master'
3849 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003850
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003851 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003852 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003853 if not options.bot:
3854 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003855
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003856 # Get try masters from PRESUBMIT.py files.
3857 masters = presubmit_support.DoGetTryMasters(
3858 change,
3859 change.LocalPaths(),
3860 settings.GetRoot(),
3861 None,
3862 None,
3863 options.verbose,
3864 sys.stdout)
3865 if masters:
3866 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003867
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003868 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3869 options.bot = presubmit_support.DoGetTrySlaves(
3870 change,
3871 change.LocalPaths(),
3872 settings.GetRoot(),
3873 None,
3874 None,
3875 options.verbose,
3876 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003877
3878 if not options.bot:
3879 # Get try masters from cq.cfg if any.
3880 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3881 # location.
3882 cq_cfg = os.path.join(change.RepositoryRoot(),
3883 'infra', 'config', 'cq.cfg')
3884 if os.path.exists(cq_cfg):
3885 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003886 cq_masters = commit_queue.get_master_builder_map(
3887 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003888 for master, builders in cq_masters.iteritems():
3889 for builder in builders:
3890 # Skip presubmit builders, because these will fail without LGTM.
3891 if 'presubmit' not in builder.lower():
3892 masters.setdefault(master, {})[builder] = ['defaulttests']
3893 if masters:
3894 return masters
3895
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003896 if not options.bot:
3897 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003898
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003899 builders_and_tests = {}
3900 # TODO(machenbach): The old style command-line options don't support
3901 # multiple try masters yet.
3902 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3903 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3904
3905 for bot in old_style:
3906 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003907 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003908 elif ',' in bot:
3909 parser.error('Specify one bot per --bot flag')
3910 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003911 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003912
3913 for bot, tests in new_style:
3914 builders_and_tests.setdefault(bot, []).extend(tests)
3915
3916 # Return a master map with one master to be backwards compatible. The
3917 # master name defaults to an empty string, which will cause the master
3918 # not to be set on rietveld (deprecated).
3919 return {options.master: builders_and_tests}
3920
3921 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003922
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003923 for builders in masters.itervalues():
3924 if any('triggered' in b for b in builders):
3925 print >> sys.stderr, (
3926 'ERROR You are trying to send a job to a triggered bot. This type of'
3927 ' bot requires an\ninitial job from a parent (usually a builder). '
3928 'Instead send your job to the parent.\n'
3929 'Bot list: %s' % builders)
3930 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003931
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003932 patchset = cl.GetMostRecentPatchset()
3933 if patchset and patchset != cl.GetPatchset():
3934 print(
3935 '\nWARNING Mismatch between local config and server. Did a previous '
3936 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3937 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003938 if options.luci:
3939 trigger_luci_job(cl, masters, options)
3940 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003941 try:
3942 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3943 except BuildbucketResponseException as ex:
3944 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003945 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003946 except Exception as e:
3947 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3948 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3949 e, stacktrace)
3950 return 1
3951 else:
3952 try:
3953 cl.RpcServer().trigger_distributed_try_jobs(
3954 cl.GetIssue(), patchset, options.name, options.clobber,
3955 options.revision, masters)
3956 except urllib2.HTTPError as e:
3957 if e.code == 404:
3958 print('404 from rietveld; '
3959 'did you mean to use "git try" instead of "git cl try"?')
3960 return 1
3961 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003962
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003963 for (master, builders) in sorted(masters.iteritems()):
3964 if master:
3965 print 'Master: %s' % master
3966 length = max(len(builder) for builder in builders)
3967 for builder in sorted(builders):
3968 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003969 return 0
3970
3971
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003972def CMDtry_results(parser, args):
3973 group = optparse.OptionGroup(parser, "Try job results options")
3974 group.add_option(
3975 "-p", "--patchset", type=int, help="patchset number if not current.")
3976 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003977 "--print-master", action='store_true', help="print master name as well.")
3978 group.add_option(
3979 "--color", action='store_true', default=sys.stdout.isatty(),
3980 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003981 group.add_option(
3982 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3983 help="Host of buildbucket. The default host is %default.")
3984 parser.add_option_group(group)
3985 auth.add_auth_options(parser)
3986 options, args = parser.parse_args(args)
3987 if args:
3988 parser.error('Unrecognized args: %s' % ' '.join(args))
3989
3990 auth_config = auth.extract_auth_config_from_options(options)
3991 cl = Changelist(auth_config=auth_config)
3992 if not cl.GetIssue():
3993 parser.error('Need to upload first')
3994
3995 if not options.patchset:
3996 options.patchset = cl.GetMostRecentPatchset()
3997 if options.patchset and options.patchset != cl.GetPatchset():
3998 print(
3999 '\nWARNING Mismatch between local config and server. Did a previous '
4000 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4001 'Continuing using\npatchset %s.\n' % options.patchset)
4002 try:
4003 jobs = fetch_try_jobs(auth_config, cl, options)
4004 except BuildbucketResponseException as ex:
4005 print 'Buildbucket error: %s' % ex
4006 return 1
4007 except Exception as e:
4008 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4009 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4010 e, stacktrace)
4011 return 1
4012 print_tryjobs(options, jobs)
4013 return 0
4014
4015
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004016@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004017def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004018 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004019 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004020 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004021 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004022
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004024 if args:
4025 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004026 branch = cl.GetBranch()
4027 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004028 cl = Changelist()
4029 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004030
4031 # Clear configured merge-base, if there is one.
4032 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004033 else:
4034 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 return 0
4036
4037
thestig@chromium.org00858c82013-12-02 23:08:03 +00004038def CMDweb(parser, args):
4039 """Opens the current CL in the web browser."""
4040 _, args = parser.parse_args(args)
4041 if args:
4042 parser.error('Unrecognized args: %s' % ' '.join(args))
4043
4044 issue_url = Changelist().GetIssueURL()
4045 if not issue_url:
4046 print >> sys.stderr, 'ERROR No issue to open'
4047 return 1
4048
4049 webbrowser.open(issue_url)
4050 return 0
4051
4052
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004053def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004054 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004055 auth.add_auth_options(parser)
4056 options, args = parser.parse_args(args)
4057 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004058 if args:
4059 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004060 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004061 props = cl.GetIssueProperties()
4062 if props.get('private'):
4063 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004064 cl.SetFlag('commit', '1')
4065 return 0
4066
4067
groby@chromium.org411034a2013-02-26 15:12:01 +00004068def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004069 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004070 auth.add_auth_options(parser)
4071 options, args = parser.parse_args(args)
4072 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004073 if args:
4074 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004075 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004076 # Ensure there actually is an issue to close.
4077 cl.GetDescription()
4078 cl.CloseIssue()
4079 return 0
4080
4081
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004082def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004083 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004084 auth.add_auth_options(parser)
4085 options, args = parser.parse_args(args)
4086 auth_config = auth.extract_auth_config_from_options(options)
4087 if args:
4088 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004089
4090 # Uncommitted (staged and unstaged) changes will be destroyed by
4091 # "git reset --hard" if there are merging conflicts in PatchIssue().
4092 # Staged changes would be committed along with the patch from last
4093 # upload, hence counted toward the "last upload" side in the final
4094 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004095 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004096 return 1
4097
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004098 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004099 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004100 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004101 if not issue:
4102 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004103 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004104 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004105
4106 # Create a new branch based on the merge-base
4107 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4108 try:
4109 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004110 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004111 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004112 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004113 return rtn
4114
wychen@chromium.org06928532015-02-03 02:11:29 +00004115 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004116 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004117 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004118 finally:
4119 RunGit(['checkout', '-q', branch])
4120 RunGit(['branch', '-D', TMP_BRANCH])
4121
4122 return 0
4123
4124
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004125def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004126 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004127 parser.add_option(
4128 '--no-color',
4129 action='store_true',
4130 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004131 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004132 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004133 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004134
4135 author = RunGit(['config', 'user.email']).strip() or None
4136
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004137 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004138
4139 if args:
4140 if len(args) > 1:
4141 parser.error('Unknown args')
4142 base_branch = args[0]
4143 else:
4144 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004145 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004146
4147 change = cl.GetChange(base_branch, None)
4148 return owners_finder.OwnersFinder(
4149 [f.LocalPath() for f in
4150 cl.GetChange(base_branch, None).AffectedFiles()],
4151 change.RepositoryRoot(), author,
4152 fopen=file, os_path=os.path, glob=glob.glob,
4153 disable_color=options.no_color).run()
4154
4155
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004156def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004157 """Generates a diff command."""
4158 # Generate diff for the current branch's changes.
4159 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4160 upstream_commit, '--' ]
4161
4162 if args:
4163 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004164 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004165 diff_cmd.append(arg)
4166 else:
4167 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004168
4169 return diff_cmd
4170
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004171def MatchingFileType(file_name, extensions):
4172 """Returns true if the file name ends with one of the given extensions."""
4173 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004174
enne@chromium.org555cfe42014-01-29 18:21:39 +00004175@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004176def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004177 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004178 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004179 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004180 parser.add_option('--full', action='store_true',
4181 help='Reformat the full content of all touched files')
4182 parser.add_option('--dry-run', action='store_true',
4183 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004184 parser.add_option('--python', action='store_true',
4185 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004186 parser.add_option('--diff', action='store_true',
4187 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004188 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004189
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004190 # git diff generates paths against the root of the repository. Change
4191 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004192 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004193 if rel_base_path:
4194 os.chdir(rel_base_path)
4195
digit@chromium.org29e47272013-05-17 17:01:46 +00004196 # Grab the merge-base commit, i.e. the upstream commit of the current
4197 # branch when it was created or the last time it was rebased. This is
4198 # to cover the case where the user may have called "git fetch origin",
4199 # moving the origin branch to a newer commit, but hasn't rebased yet.
4200 upstream_commit = None
4201 cl = Changelist()
4202 upstream_branch = cl.GetUpstreamBranch()
4203 if upstream_branch:
4204 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4205 upstream_commit = upstream_commit.strip()
4206
4207 if not upstream_commit:
4208 DieWithError('Could not find base commit for this branch. '
4209 'Are you in detached state?')
4210
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004211 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4212 diff_output = RunGit(changed_files_cmd)
4213 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004214 # Filter out files deleted by this CL
4215 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004216
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004217 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4218 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4219 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004220 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004221
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004222 top_dir = os.path.normpath(
4223 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4224
4225 # Locate the clang-format binary in the checkout
4226 try:
4227 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4228 except clang_format.NotFoundError, e:
4229 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004230
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004231 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4232 # formatted. This is used to block during the presubmit.
4233 return_value = 0
4234
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004235 if clang_diff_files:
4236 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004237 cmd = [clang_format_tool]
4238 if not opts.dry_run and not opts.diff:
4239 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004240 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004241 if opts.diff:
4242 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004243 else:
4244 env = os.environ.copy()
4245 env['PATH'] = str(os.path.dirname(clang_format_tool))
4246 try:
4247 script = clang_format.FindClangFormatScriptInChromiumTree(
4248 'clang-format-diff.py')
4249 except clang_format.NotFoundError, e:
4250 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004251
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004252 cmd = [sys.executable, script, '-p0']
4253 if not opts.dry_run and not opts.diff:
4254 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004255
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004256 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4257 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004258
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004259 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4260 if opts.diff:
4261 sys.stdout.write(stdout)
4262 if opts.dry_run and len(stdout) > 0:
4263 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004264
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004265 # Similar code to above, but using yapf on .py files rather than clang-format
4266 # on C/C++ files
4267 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004268 yapf_tool = gclient_utils.FindExecutable('yapf')
4269 if yapf_tool is None:
4270 DieWithError('yapf not found in PATH')
4271
4272 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004273 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004274 cmd = [yapf_tool]
4275 if not opts.dry_run and not opts.diff:
4276 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004277 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004278 if opts.diff:
4279 sys.stdout.write(stdout)
4280 else:
4281 # TODO(sbc): yapf --lines mode still has some issues.
4282 # https://github.com/google/yapf/issues/154
4283 DieWithError('--python currently only works with --full')
4284
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004285 # Dart's formatter does not have the nice property of only operating on
4286 # modified chunks, so hard code full.
4287 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004288 try:
4289 command = [dart_format.FindDartFmtToolInChromiumTree()]
4290 if not opts.dry_run and not opts.diff:
4291 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004292 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004293
ppi@chromium.org6593d932016-03-03 15:41:15 +00004294 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004295 if opts.dry_run and stdout:
4296 return_value = 2
4297 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004298 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4299 'found in this checkout. Files in other languages are still ' +
4300 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004301
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004302 # Format GN build files. Always run on full build files for canonical form.
4303 if gn_diff_files:
4304 cmd = ['gn', 'format']
4305 if not opts.dry_run and not opts.diff:
4306 cmd.append('--in-place')
4307 for gn_diff_file in gn_diff_files:
4308 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4309 if opts.diff:
4310 sys.stdout.write(stdout)
4311
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004312 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004313
4314
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004315@subcommand.usage('<codereview url or issue id>')
4316def CMDcheckout(parser, args):
4317 """Checks out a branch associated with a given Rietveld issue."""
4318 _, args = parser.parse_args(args)
4319
4320 if len(args) != 1:
4321 parser.print_help()
4322 return 1
4323
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004324 target_issue = ParseIssueNum(args[0])
4325 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004326 parser.print_help()
4327 return 1
4328
4329 key_and_issues = [x.split() for x in RunGit(
4330 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4331 .splitlines()]
4332 branches = []
4333 for key, issue in key_and_issues:
4334 if issue == target_issue:
4335 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4336
4337 if len(branches) == 0:
4338 print 'No branch found for issue %s.' % target_issue
4339 return 1
4340 if len(branches) == 1:
4341 RunGit(['checkout', branches[0]])
4342 else:
4343 print 'Multiple branches match issue %s:' % target_issue
4344 for i in range(len(branches)):
4345 print '%d: %s' % (i, branches[i])
4346 which = raw_input('Choose by index: ')
4347 try:
4348 RunGit(['checkout', branches[int(which)]])
4349 except (IndexError, ValueError):
4350 print 'Invalid selection, not checking out any branch.'
4351 return 1
4352
4353 return 0
4354
4355
maruel@chromium.org29404b52014-09-08 22:58:00 +00004356def CMDlol(parser, args):
4357 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004358 print zlib.decompress(base64.b64decode(
4359 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4360 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4361 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4362 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004363 return 0
4364
4365
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004366class OptionParser(optparse.OptionParser):
4367 """Creates the option parse and add --verbose support."""
4368 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004369 optparse.OptionParser.__init__(
4370 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004371 self.add_option(
4372 '-v', '--verbose', action='count', default=0,
4373 help='Use 2 times for more debugging info')
4374
4375 def parse_args(self, args=None, values=None):
4376 options, args = optparse.OptionParser.parse_args(self, args, values)
4377 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4378 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4379 return options, args
4380
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004381
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004382def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004383 if sys.hexversion < 0x02060000:
4384 print >> sys.stderr, (
4385 '\nYour python version %s is unsupported, please upgrade.\n' %
4386 sys.version.split(' ', 1)[0])
4387 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004388
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004389 # Reload settings.
4390 global settings
4391 settings = Settings()
4392
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004393 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004394 dispatcher = subcommand.CommandDispatcher(__name__)
4395 try:
4396 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004397 except auth.AuthenticationError as e:
4398 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004399 except urllib2.HTTPError, e:
4400 if e.code != 500:
4401 raise
4402 DieWithError(
4403 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4404 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004405 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406
4407
4408if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004409 # These affect sys.stdout so do it outside of main() to simplify mocks in
4410 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004411 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004412 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004413 try:
4414 sys.exit(main(sys.argv[1:]))
4415 except KeyboardInterrupt:
4416 sys.stderr.write('interrupted\n')
4417 sys.exit(1)