blob: 8eda0dc6247d6a75d76174abd0b43ccddc5e9c80 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000042from luci_hacks import trigger_luci_job as luci_trigger
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000047import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000048import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000049import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000050from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000051import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000054import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000056import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import watchlists
59
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000062DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000063POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000065GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000066CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000067REFS_THAT_ALIAS_TO_OTHER_REFS = {
68 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
69 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
70}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000142def BranchExists(branch):
143 """Return True if specified branch exists."""
144 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
145 suppress_stderr=True)
146 return not code
147
148
maruel@chromium.org90541732011-04-01 17:54:18 +0000149def ask_for_data(prompt):
150 try:
151 return raw_input(prompt)
152 except KeyboardInterrupt:
153 # Hide the exception.
154 sys.exit(1)
155
156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157def git_set_branch_value(key, value):
158 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000159 if not branch:
160 return
161
162 cmd = ['config']
163 if isinstance(value, int):
164 cmd.append('--int')
165 git_key = 'branch.%s.%s' % (branch, key)
166 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167
168
169def git_get_branch_default(key, default):
170 branch = Changelist().GetBranch()
171 if branch:
172 git_key = 'branch.%s.%s' % (branch, key)
173 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
174 try:
175 return int(stdout.strip())
176 except ValueError:
177 pass
178 return default
179
180
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181def add_git_similarity(parser):
182 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 help='Sets the percentage that a pair of files need to match in order to'
185 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 parser.add_option(
187 '--find-copies', action='store_true',
188 help='Allows git to look for copies.')
189 parser.add_option(
190 '--no-find-copies', action='store_false', dest='find_copies',
191 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 old_parser_args = parser.parse_args
194 def Parse(args):
195 options, args = old_parser_args(args)
196
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 print('Note: Saving similarity of %d%% in git config.'
201 % options.similarity)
202 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 options.similarity = max(0, min(options.similarity, 100))
205
206 if options.find_copies is None:
207 options.find_copies = bool(
208 git_get_branch_default('git-find-copies', True))
209 else:
210 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000211
212 print('Using %d%% similarity for rename/copy detection. '
213 'Override with --similarity.' % options.similarity)
214
215 return options, args
216 parser.parse_args = Parse
217
218
machenbach@chromium.org45453142015-09-15 08:45:22 +0000219def _get_properties_from_options(options):
220 properties = dict(x.split('=', 1) for x in options.properties)
221 for key, val in properties.iteritems():
222 try:
223 properties[key] = json.loads(val)
224 except ValueError:
225 pass # If a value couldn't be evaluated, treat it as a string.
226 return properties
227
228
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000229def _prefix_master(master):
230 """Convert user-specified master name to full master name.
231
232 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
233 name, while the developers always use shortened master name
234 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
235 function does the conversion for buildbucket migration.
236 """
237 prefix = 'master.'
238 if master.startswith(prefix):
239 return master
240 return '%s%s' % (prefix, master)
241
242
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000243def trigger_luci_job(changelist, masters, options):
244 """Send a job to run on LUCI."""
245 issue_props = changelist.GetIssueProperties()
246 issue = changelist.GetIssue()
247 patchset = changelist.GetMostRecentPatchset()
248 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000249 # TODO(hinoka et al): add support for other properties.
250 # Currently, this completely ignores testfilter and other properties.
251 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000252 luci_trigger.trigger(
253 builder, 'HEAD', issue, patchset, issue_props['project'])
254
255
machenbach@chromium.org45453142015-09-15 08:45:22 +0000256def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000257 rietveld_url = settings.GetDefaultServerUrl()
258 rietveld_host = urlparse.urlparse(rietveld_url).hostname
259 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
260 http = authenticator.authorize(httplib2.Http())
261 http.force_exception_to_status_code = True
262 issue_props = changelist.GetIssueProperties()
263 issue = changelist.GetIssue()
264 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000265 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000266
267 buildbucket_put_url = (
268 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000269 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
271 hostname=rietveld_host,
272 issue=issue,
273 patch=patchset)
274
275 batch_req_body = {'builds': []}
276 print_text = []
277 print_text.append('Tried jobs on:')
278 for master, builders_and_tests in sorted(masters.iteritems()):
279 print_text.append('Master: %s' % master)
280 bucket = _prefix_master(master)
281 for builder, tests in sorted(builders_and_tests.iteritems()):
282 print_text.append(' %s: %s' % (builder, tests))
283 parameters = {
284 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000285 'changes': [{
286 'author': {'email': issue_props['owner_email']},
287 'revision': options.revision,
288 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 'properties': {
290 'category': category,
291 'issue': issue,
292 'master': master,
293 'patch_project': issue_props['project'],
294 'patch_storage': 'rietveld',
295 'patchset': patchset,
296 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 },
299 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000300 if tests:
301 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000302 if properties:
303 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304 if options.clobber:
305 parameters['properties']['clobber'] = True
306 batch_req_body['builds'].append(
307 {
308 'bucket': bucket,
309 'parameters_json': json.dumps(parameters),
310 'tags': ['builder:%s' % builder,
311 'buildset:%s' % buildset,
312 'master:%s' % master,
313 'user_agent:git_cl_try']
314 }
315 )
316
317 for try_count in xrange(3):
318 response, content = http.request(
319 buildbucket_put_url,
320 'PUT',
321 body=json.dumps(batch_req_body),
322 headers={'Content-Type': 'application/json'},
323 )
324 content_json = None
325 try:
326 content_json = json.loads(content)
327 except ValueError:
328 pass
329
330 # Buildbucket could return an error even if status==200.
331 if content_json and content_json.get('error'):
332 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
333 content_json['error'].get('code', ''),
334 content_json['error'].get('reason', ''),
335 content_json['error'].get('message', ''))
336 raise BuildbucketResponseException(msg)
337
338 if response.status == 200:
339 if not content_json:
340 raise BuildbucketResponseException(
341 'Buildbucket returns invalid json content: %s.\n'
342 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
343 content)
344 break
345 if response.status < 500 or try_count >= 2:
346 raise httplib2.HttpLib2Error(content)
347
348 # status >= 500 means transient failures.
349 logging.debug('Transient errors when triggering tryjobs. Will retry.')
350 time.sleep(0.5 + 1.5*try_count)
351
352 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000353
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000355def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
356 """Return the corresponding git ref if |base_url| together with |glob_spec|
357 matches the full |url|.
358
359 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
360 """
361 fetch_suburl, as_ref = glob_spec.split(':')
362 if allow_wildcards:
363 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
364 if glob_match:
365 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
366 # "branches/{472,597,648}/src:refs/remotes/svn/*".
367 branch_re = re.escape(base_url)
368 if glob_match.group(1):
369 branch_re += '/' + re.escape(glob_match.group(1))
370 wildcard = glob_match.group(2)
371 if wildcard == '*':
372 branch_re += '([^/]*)'
373 else:
374 # Escape and replace surrounding braces with parentheses and commas
375 # with pipe symbols.
376 wildcard = re.escape(wildcard)
377 wildcard = re.sub('^\\\\{', '(', wildcard)
378 wildcard = re.sub('\\\\,', '|', wildcard)
379 wildcard = re.sub('\\\\}$', ')', wildcard)
380 branch_re += wildcard
381 if glob_match.group(3):
382 branch_re += re.escape(glob_match.group(3))
383 match = re.match(branch_re, url)
384 if match:
385 return re.sub('\*$', match.group(1), as_ref)
386
387 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
388 if fetch_suburl:
389 full_url = base_url + '/' + fetch_suburl
390 else:
391 full_url = base_url
392 if full_url == url:
393 return as_ref
394 return None
395
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000396
iannucci@chromium.org79540052012-10-19 23:15:26 +0000397def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000398 """Prints statistics about the change to the user."""
399 # --no-ext-diff is broken in some versions of Git, so try to work around
400 # this by overriding the environment (but there is still a problem if the
401 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000402 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000403 if 'GIT_EXTERNAL_DIFF' in env:
404 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000405
406 if find_copies:
407 similarity_options = ['--find-copies-harder', '-l100000',
408 '-C%s' % similarity]
409 else:
410 similarity_options = ['-M%s' % similarity]
411
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000412 try:
413 stdout = sys.stdout.fileno()
414 except AttributeError:
415 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000416 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000417 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000418 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000419 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000420
421
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000422class BuildbucketResponseException(Exception):
423 pass
424
425
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000426class Settings(object):
427 def __init__(self):
428 self.default_server = None
429 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000430 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 self.is_git_svn = None
432 self.svn_branch = None
433 self.tree_status_url = None
434 self.viewvc_url = None
435 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000436 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000437 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000438 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000439 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000440 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000441 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442
443 def LazyUpdateIfNeeded(self):
444 """Updates the settings from a codereview.settings file, if available."""
445 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000446 # The only value that actually changes the behavior is
447 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000448 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000449 error_ok=True
450 ).strip().lower()
451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000453 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000455 # set updated to True to avoid infinite calling loop
456 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000457 self.updated = True
458 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459 self.updated = True
460
461 def GetDefaultServerUrl(self, error_ok=False):
462 if not self.default_server:
463 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000464 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000465 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 if error_ok:
467 return self.default_server
468 if not self.default_server:
469 error_message = ('Could not find settings file. You must configure '
470 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000471 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000472 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000473 return self.default_server
474
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000475 @staticmethod
476 def GetRelativeRoot():
477 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000480 if self.root is None:
481 self.root = os.path.abspath(self.GetRelativeRoot())
482 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483
484 def GetIsGitSvn(self):
485 """Return true if this repo looks like it's using git-svn."""
486 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000487 if self.GetPendingRefPrefix():
488 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
489 self.is_git_svn = False
490 else:
491 # If you have any "svn-remote.*" config keys, we think you're using svn.
492 self.is_git_svn = RunGitWithCode(
493 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 return self.is_git_svn
495
496 def GetSVNBranch(self):
497 if self.svn_branch is None:
498 if not self.GetIsGitSvn():
499 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
500
501 # Try to figure out which remote branch we're based on.
502 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000503 # 1) iterate through our branch history and find the svn URL.
504 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505
506 # regexp matching the git-svn line that contains the URL.
507 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
508
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000509 # We don't want to go through all of history, so read a line from the
510 # pipe at a time.
511 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000512 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000513 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
514 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000515 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000516 for line in proc.stdout:
517 match = git_svn_re.match(line)
518 if match:
519 url = match.group(1)
520 proc.stdout.close() # Cut pipe.
521 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000523 if url:
524 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
525 remotes = RunGit(['config', '--get-regexp',
526 r'^svn-remote\..*\.url']).splitlines()
527 for remote in remotes:
528 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000529 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000530 remote = match.group(1)
531 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000532 rewrite_root = RunGit(
533 ['config', 'svn-remote.%s.rewriteRoot' % remote],
534 error_ok=True).strip()
535 if rewrite_root:
536 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000537 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000538 ['config', 'svn-remote.%s.fetch' % remote],
539 error_ok=True).strip()
540 if fetch_spec:
541 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
542 if self.svn_branch:
543 break
544 branch_spec = RunGit(
545 ['config', 'svn-remote.%s.branches' % remote],
546 error_ok=True).strip()
547 if branch_spec:
548 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
549 if self.svn_branch:
550 break
551 tag_spec = RunGit(
552 ['config', 'svn-remote.%s.tags' % remote],
553 error_ok=True).strip()
554 if tag_spec:
555 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
556 if self.svn_branch:
557 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558
559 if not self.svn_branch:
560 DieWithError('Can\'t guess svn branch -- try specifying it on the '
561 'command line')
562
563 return self.svn_branch
564
565 def GetTreeStatusUrl(self, error_ok=False):
566 if not self.tree_status_url:
567 error_message = ('You must configure your tree status URL by running '
568 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000569 self.tree_status_url = self._GetRietveldConfig(
570 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 return self.tree_status_url
572
573 def GetViewVCUrl(self):
574 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000575 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 return self.viewvc_url
577
rmistry@google.com90752582014-01-14 21:04:50 +0000578 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000579 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000580
rmistry@google.com78948ed2015-07-08 23:09:57 +0000581 def GetIsSkipDependencyUpload(self, branch_name):
582 """Returns true if specified branch should skip dep uploads."""
583 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
584 error_ok=True)
585
rmistry@google.com5626a922015-02-26 14:03:30 +0000586 def GetRunPostUploadHook(self):
587 run_post_upload_hook = self._GetRietveldConfig(
588 'run-post-upload-hook', error_ok=True)
589 return run_post_upload_hook == "True"
590
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000591 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000592 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000593
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000594 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000595 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000596
ukai@chromium.orge8077812012-02-03 03:41:46 +0000597 def GetIsGerrit(self):
598 """Return true if this repo is assosiated with gerrit code review system."""
599 if self.is_gerrit is None:
600 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
601 return self.is_gerrit
602
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000603 def GetSquashGerritUploads(self):
604 """Return true if uploads to Gerrit should be squashed by default."""
605 if self.squash_gerrit_uploads is None:
606 self.squash_gerrit_uploads = (
607 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
608 error_ok=True).strip() == 'true')
609 return self.squash_gerrit_uploads
610
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000611 def GetGitEditor(self):
612 """Return the editor specified in the git config, or None if none is."""
613 if self.git_editor is None:
614 self.git_editor = self._GetConfig('core.editor', error_ok=True)
615 return self.git_editor or None
616
thestig@chromium.org44202a22014-03-11 19:22:18 +0000617 def GetLintRegex(self):
618 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
619 DEFAULT_LINT_REGEX)
620
621 def GetLintIgnoreRegex(self):
622 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
623 DEFAULT_LINT_IGNORE_REGEX)
624
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000625 def GetProject(self):
626 if not self.project:
627 self.project = self._GetRietveldConfig('project', error_ok=True)
628 return self.project
629
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000630 def GetForceHttpsCommitUrl(self):
631 if not self.force_https_commit_url:
632 self.force_https_commit_url = self._GetRietveldConfig(
633 'force-https-commit-url', error_ok=True)
634 return self.force_https_commit_url
635
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000636 def GetPendingRefPrefix(self):
637 if not self.pending_ref_prefix:
638 self.pending_ref_prefix = self._GetRietveldConfig(
639 'pending-ref-prefix', error_ok=True)
640 return self.pending_ref_prefix
641
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000642 def _GetRietveldConfig(self, param, **kwargs):
643 return self._GetConfig('rietveld.' + param, **kwargs)
644
rmistry@google.com78948ed2015-07-08 23:09:57 +0000645 def _GetBranchConfig(self, branch_name, param, **kwargs):
646 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
647
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000648 def _GetConfig(self, param, **kwargs):
649 self.LazyUpdateIfNeeded()
650 return RunGit(['config', param], **kwargs).strip()
651
652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653def ShortBranchName(branch):
654 """Convert a name like 'refs/heads/foo' to just 'foo'."""
655 return branch.replace('refs/heads/', '')
656
657
658class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000659 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000661 global settings
662 if not settings:
663 # Happens when git_cl.py is used as a utility library.
664 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 settings.GetDefaultServerUrl()
666 self.branchref = branchref
667 if self.branchref:
668 self.branch = ShortBranchName(self.branchref)
669 else:
670 self.branch = None
671 self.rietveld_server = None
672 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000673 self.lookedup_issue = False
674 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 self.has_description = False
676 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000677 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000679 self.cc = None
680 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000681 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000682 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000683 self._remote = None
684 self._rpc_server = None
685
686 @property
687 def auth_config(self):
688 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000689
690 def GetCCList(self):
691 """Return the users cc'd on this CL.
692
693 Return is a string suitable for passing to gcl with the --cc flag.
694 """
695 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000696 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000697 more_cc = ','.join(self.watchers)
698 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
699 return self.cc
700
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000701 def GetCCListWithoutDefault(self):
702 """Return the users cc'd on this CL excluding default ones."""
703 if self.cc is None:
704 self.cc = ','.join(self.watchers)
705 return self.cc
706
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000707 def SetWatchers(self, watchers):
708 """Set the list of email addresses that should be cc'd based on the changed
709 files in this CL.
710 """
711 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712
713 def GetBranch(self):
714 """Returns the short branch name, e.g. 'master'."""
715 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000716 branchref = RunGit(['symbolic-ref', 'HEAD'],
717 stderr=subprocess2.VOID, error_ok=True).strip()
718 if not branchref:
719 return None
720 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721 self.branch = ShortBranchName(self.branchref)
722 return self.branch
723
724 def GetBranchRef(self):
725 """Returns the full branch name, e.g. 'refs/heads/master'."""
726 self.GetBranch() # Poke the lazy loader.
727 return self.branchref
728
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000729 @staticmethod
730 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000731 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 e.g. 'origin', 'refs/heads/master'
733 """
734 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
736 error_ok=True).strip()
737 if upstream_branch:
738 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
739 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000740 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
741 error_ok=True).strip()
742 if upstream_branch:
743 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000745 # Fall back on trying a git-svn upstream branch.
746 if settings.GetIsGitSvn():
747 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000749 # Else, try to guess the origin remote.
750 remote_branches = RunGit(['branch', '-r']).split()
751 if 'origin/master' in remote_branches:
752 # Fall back on origin/master if it exits.
753 remote = 'origin'
754 upstream_branch = 'refs/heads/master'
755 elif 'origin/trunk' in remote_branches:
756 # Fall back on origin/trunk if it exists. Generally a shared
757 # git-svn clone
758 remote = 'origin'
759 upstream_branch = 'refs/heads/trunk'
760 else:
761 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762Either pass complete "git diff"-style arguments, like
763 git cl upload origin/master
764or verify this branch is set up to track another (via the --track argument to
765"git checkout -b ...").""")
766
767 return remote, upstream_branch
768
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000769 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000770 upstream_branch = self.GetUpstreamBranch()
771 if not BranchExists(upstream_branch):
772 DieWithError('The upstream for the current branch (%s) does not exist '
773 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000774 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000775 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000776
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777 def GetUpstreamBranch(self):
778 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000779 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000781 upstream_branch = upstream_branch.replace('refs/heads/',
782 'refs/remotes/%s/' % remote)
783 upstream_branch = upstream_branch.replace('refs/branch-heads/',
784 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 self.upstream_branch = upstream_branch
786 return self.upstream_branch
787
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000788 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000789 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000790 remote, branch = None, self.GetBranch()
791 seen_branches = set()
792 while branch not in seen_branches:
793 seen_branches.add(branch)
794 remote, branch = self.FetchUpstreamTuple(branch)
795 branch = ShortBranchName(branch)
796 if remote != '.' or branch.startswith('refs/remotes'):
797 break
798 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000799 remotes = RunGit(['remote'], error_ok=True).split()
800 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000801 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000802 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000803 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000804 logging.warning('Could not determine which remote this change is '
805 'associated with, so defaulting to "%s". This may '
806 'not be what you want. You may prevent this message '
807 'by running "git svn info" as documented here: %s',
808 self._remote,
809 GIT_INSTRUCTIONS_URL)
810 else:
811 logging.warn('Could not determine which remote this change is '
812 'associated with. You may prevent this message by '
813 'running "git svn info" as documented here: %s',
814 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000815 branch = 'HEAD'
816 if branch.startswith('refs/remotes'):
817 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000818 elif branch.startswith('refs/branch-heads/'):
819 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000820 else:
821 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000822 return self._remote
823
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000824 def GitSanityChecks(self, upstream_git_obj):
825 """Checks git repo status and ensures diff is from local commits."""
826
sbc@chromium.org79706062015-01-14 21:18:12 +0000827 if upstream_git_obj is None:
828 if self.GetBranch() is None:
829 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000830 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000831 else:
832 print >> sys.stderr, (
833 'ERROR: no upstream branch')
834 return False
835
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000836 # Verify the commit we're diffing against is in our current branch.
837 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
838 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
839 if upstream_sha != common_ancestor:
840 print >> sys.stderr, (
841 'ERROR: %s is not in the current branch. You may need to rebase '
842 'your tracking branch' % upstream_sha)
843 return False
844
845 # List the commits inside the diff, and verify they are all local.
846 commits_in_diff = RunGit(
847 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
848 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
849 remote_branch = remote_branch.strip()
850 if code != 0:
851 _, remote_branch = self.GetRemoteBranch()
852
853 commits_in_remote = RunGit(
854 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
855
856 common_commits = set(commits_in_diff) & set(commits_in_remote)
857 if common_commits:
858 print >> sys.stderr, (
859 'ERROR: Your diff contains %d commits already in %s.\n'
860 'Run "git log --oneline %s..HEAD" to get a list of commits in '
861 'the diff. If you are using a custom git flow, you can override'
862 ' the reference used for this check with "git config '
863 'gitcl.remotebranch <git-ref>".' % (
864 len(common_commits), remote_branch, upstream_git_obj))
865 return False
866 return True
867
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000868 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000869 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000870
871 Returns None if it is not set.
872 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000873 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
874 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000875
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000876 def GetGitSvnRemoteUrl(self):
877 """Return the configured git-svn remote URL parsed from git svn info.
878
879 Returns None if it is not set.
880 """
881 # URL is dependent on the current directory.
882 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
883 if data:
884 keys = dict(line.split(': ', 1) for line in data.splitlines()
885 if ': ' in line)
886 return keys.get('URL', None)
887 return None
888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 def GetRemoteUrl(self):
890 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
891
892 Returns None if there is no remote.
893 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000894 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000895 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
896
897 # If URL is pointing to a local directory, it is probably a git cache.
898 if os.path.isdir(url):
899 url = RunGit(['config', 'remote.%s.url' % remote],
900 error_ok=True,
901 cwd=url).strip()
902 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903
904 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000905 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000906 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000908 self.issue = int(issue) or None if issue else None
909 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 return self.issue
911
912 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000913 if not self.rietveld_server:
914 # If we're on a branch then get the server potentially associated
915 # with that branch.
916 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000917 rietveld_server_config = self._RietveldServer()
918 if rietveld_server_config:
919 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
920 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000921 if not self.rietveld_server:
922 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923 return self.rietveld_server
924
925 def GetIssueURL(self):
926 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000927 if not self.GetIssue():
928 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
930
931 def GetDescription(self, pretty=False):
932 if not self.has_description:
933 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000934 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000935 try:
936 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000937 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000938 if e.code == 404:
939 DieWithError(
940 ('\nWhile fetching the description for issue %d, received a '
941 '404 (not found)\n'
942 'error. It is likely that you deleted this '
943 'issue on the server. If this is the\n'
944 'case, please run\n\n'
945 ' git cl issue 0\n\n'
946 'to clear the association with the deleted issue. Then run '
947 'this command again.') % issue)
948 else:
949 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000950 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000951 except urllib2.URLError as e:
952 print >> sys.stderr, (
953 'Warning: Failed to retrieve CL description due to network '
954 'failure.')
955 self.description = ''
956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 self.has_description = True
958 if pretty:
959 wrapper = textwrap.TextWrapper()
960 wrapper.initial_indent = wrapper.subsequent_indent = ' '
961 return wrapper.fill(self.description)
962 return self.description
963
964 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000965 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000966 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 patchset = RunGit(['config', self._PatchsetSetting()],
968 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000969 self.patchset = int(patchset) or None if patchset else None
970 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 return self.patchset
972
973 def SetPatchset(self, patchset):
974 """Set this branch's patchset. If patchset=0, clears the patchset."""
975 if patchset:
976 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000977 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 else:
979 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000980 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000981 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000983 def GetMostRecentPatchset(self):
984 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000985
986 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000987 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000988 '/download/issue%s_%s.diff' % (issue, patchset))
989
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000990 def GetIssueProperties(self):
991 if self._props is None:
992 issue = self.GetIssue()
993 if not issue:
994 self._props = {}
995 else:
996 self._props = self.RpcServer().get_issue_properties(issue, True)
997 return self._props
998
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000999 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001000 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001001
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001002 def AddComment(self, message):
1003 return self.RpcServer().add_comment(self.GetIssue(), message)
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 def SetIssue(self, issue):
1006 """Set this branch's issue. If issue=0, clears the issue."""
1007 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001008 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 RunGit(['config', self._IssueSetting(), str(issue)])
1010 if self.rietveld_server:
1011 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1012 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001013 current_issue = self.GetIssue()
1014 if current_issue:
1015 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001016 self.issue = None
1017 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001019 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001020 if not self.GitSanityChecks(upstream_branch):
1021 DieWithError('\nGit sanity check failure')
1022
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001023 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001024 if not root:
1025 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001026 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001027
1028 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001029 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001030 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001031 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001032 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001033 except subprocess2.CalledProcessError:
1034 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001035 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001036 'This branch probably doesn\'t exist anymore. To reset the\n'
1037 'tracking branch, please run\n'
1038 ' git branch --set-upstream %s trunk\n'
1039 'replacing trunk with origin/master or the relevant branch') %
1040 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001041
maruel@chromium.org52424302012-08-29 15:14:30 +00001042 issue = self.GetIssue()
1043 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001044 if issue:
1045 description = self.GetDescription()
1046 else:
1047 # If the change was never uploaded, use the log messages of all commits
1048 # up to the branch point, as git cl upload will prefill the description
1049 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001050 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1051 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001052
1053 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001054 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001055 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001056 name,
1057 description,
1058 absroot,
1059 files,
1060 issue,
1061 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001062 author,
1063 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001064
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001065 def GetStatus(self):
1066 """Apply a rough heuristic to give a simple summary of an issue's review
1067 or CQ status, assuming adherence to a common workflow.
1068
1069 Returns None if no issue for this branch, or one of the following keywords:
1070 * 'error' - error from review tool (including deleted issues)
1071 * 'unsent' - not sent for review
1072 * 'waiting' - waiting for review
1073 * 'reply' - waiting for owner to reply to review
1074 * 'lgtm' - LGTM from at least one approved reviewer
1075 * 'commit' - in the commit queue
1076 * 'closed' - closed
1077 """
1078 if not self.GetIssue():
1079 return None
1080
1081 try:
1082 props = self.GetIssueProperties()
1083 except urllib2.HTTPError:
1084 return 'error'
1085
1086 if props.get('closed'):
1087 # Issue is closed.
1088 return 'closed'
1089 if props.get('commit'):
1090 # Issue is in the commit queue.
1091 return 'commit'
1092
1093 try:
1094 reviewers = self.GetApprovingReviewers()
1095 except urllib2.HTTPError:
1096 return 'error'
1097
1098 if reviewers:
1099 # Was LGTM'ed.
1100 return 'lgtm'
1101
1102 messages = props.get('messages') or []
1103
1104 if not messages:
1105 # No message was sent.
1106 return 'unsent'
1107 if messages[-1]['sender'] != props.get('owner_email'):
1108 # Non-LGTM reply from non-owner
1109 return 'reply'
1110 return 'waiting'
1111
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001112 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001113 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001114
1115 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001116 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001117 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001118 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001119 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001120 except presubmit_support.PresubmitFailure, e:
1121 DieWithError(
1122 ('%s\nMaybe your depot_tools is out of date?\n'
1123 'If all fails, contact maruel@') % e)
1124
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001125 def UpdateDescription(self, description):
1126 self.description = description
1127 return self.RpcServer().update_description(
1128 self.GetIssue(), self.description)
1129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001131 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001132 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001134 def SetFlag(self, flag, value):
1135 """Patchset must match."""
1136 if not self.GetPatchset():
1137 DieWithError('The patchset needs to match. Send another patchset.')
1138 try:
1139 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001140 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001141 except urllib2.HTTPError, e:
1142 if e.code == 404:
1143 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1144 if e.code == 403:
1145 DieWithError(
1146 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1147 'match?') % (self.GetIssue(), self.GetPatchset()))
1148 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001150 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 """Returns an upload.RpcServer() to access this review's rietveld instance.
1152 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001153 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001154 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001155 self.GetRietveldServer(),
1156 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001157 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158
1159 def _IssueSetting(self):
1160 """Return the git setting that stores this change's issue."""
1161 return 'branch.%s.rietveldissue' % self.GetBranch()
1162
1163 def _PatchsetSetting(self):
1164 """Return the git setting that stores this change's most recent patchset."""
1165 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1166
1167 def _RietveldServer(self):
1168 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001169 branch = self.GetBranch()
1170 if branch:
1171 return 'branch.%s.rietveldserver' % branch
1172 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
1174
1175def GetCodereviewSettingsInteractively():
1176 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001177 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 server = settings.GetDefaultServerUrl(error_ok=True)
1179 prompt = 'Rietveld server (host[:port])'
1180 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001181 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if not server and not newserver:
1183 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001184 if newserver:
1185 newserver = gclient_utils.UpgradeToHttps(newserver)
1186 if newserver != server:
1187 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001189 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 prompt = caption
1191 if initial:
1192 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001193 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 if new_val == 'x':
1195 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001196 elif new_val:
1197 if is_url:
1198 new_val = gclient_utils.UpgradeToHttps(new_val)
1199 if new_val != initial:
1200 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001202 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001203 SetProperty(settings.GetDefaultPrivateFlag(),
1204 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001206 'tree-status-url', False)
1207 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001208 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001209 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1210 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 # TODO: configure a default branch to diff against, rather than this
1213 # svn-based hackery.
1214
1215
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001216class ChangeDescription(object):
1217 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001218 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001219 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001220
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001221 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001222 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001223
agable@chromium.org42c20792013-09-12 17:34:49 +00001224 @property # www.logilab.org/ticket/89786
1225 def description(self): # pylint: disable=E0202
1226 return '\n'.join(self._description_lines)
1227
1228 def set_description(self, desc):
1229 if isinstance(desc, basestring):
1230 lines = desc.splitlines()
1231 else:
1232 lines = [line.rstrip() for line in desc]
1233 while lines and not lines[0]:
1234 lines.pop(0)
1235 while lines and not lines[-1]:
1236 lines.pop(-1)
1237 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001238
piman@chromium.org336f9122014-09-04 02:16:55 +00001239 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001240 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001241 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001242 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001243 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001244 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001245
agable@chromium.org42c20792013-09-12 17:34:49 +00001246 # Get the set of R= and TBR= lines and remove them from the desciption.
1247 regexp = re.compile(self.R_LINE)
1248 matches = [regexp.match(line) for line in self._description_lines]
1249 new_desc = [l for i, l in enumerate(self._description_lines)
1250 if not matches[i]]
1251 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001252
agable@chromium.org42c20792013-09-12 17:34:49 +00001253 # Construct new unified R= and TBR= lines.
1254 r_names = []
1255 tbr_names = []
1256 for match in matches:
1257 if not match:
1258 continue
1259 people = cleanup_list([match.group(2).strip()])
1260 if match.group(1) == 'TBR':
1261 tbr_names.extend(people)
1262 else:
1263 r_names.extend(people)
1264 for name in r_names:
1265 if name not in reviewers:
1266 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001267 if add_owners_tbr:
1268 owners_db = owners.Database(change.RepositoryRoot(),
1269 fopen=file, os_path=os.path, glob=glob.glob)
1270 all_reviewers = set(tbr_names + reviewers)
1271 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1272 all_reviewers)
1273 tbr_names.extend(owners_db.reviewers_for(missing_files,
1274 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001275 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1276 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1277
1278 # Put the new lines in the description where the old first R= line was.
1279 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1280 if 0 <= line_loc < len(self._description_lines):
1281 if new_tbr_line:
1282 self._description_lines.insert(line_loc, new_tbr_line)
1283 if new_r_line:
1284 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001285 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001286 if new_r_line:
1287 self.append_footer(new_r_line)
1288 if new_tbr_line:
1289 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290
1291 def prompt(self):
1292 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001293 self.set_description([
1294 '# Enter a description of the change.',
1295 '# This will be displayed on the codereview site.',
1296 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001297 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001298 '--------------------',
1299 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001300
agable@chromium.org42c20792013-09-12 17:34:49 +00001301 regexp = re.compile(self.BUG_LINE)
1302 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001303 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001304 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001305 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001306 if not content:
1307 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001308 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001309
1310 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001311 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1312 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001313 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001314 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001315
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001316 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001317 if self._description_lines:
1318 # Add an empty line if either the last line or the new line isn't a tag.
1319 last_line = self._description_lines[-1]
1320 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1321 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1322 self._description_lines.append('')
1323 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001324
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001325 def get_reviewers(self):
1326 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001327 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1328 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001329 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001330
1331
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001332def get_approving_reviewers(props):
1333 """Retrieves the reviewers that approved a CL from the issue properties with
1334 messages.
1335
1336 Note that the list may contain reviewers that are not committer, thus are not
1337 considered by the CQ.
1338 """
1339 return sorted(
1340 set(
1341 message['sender']
1342 for message in props['messages']
1343 if message['approval'] and message['sender'] in props['reviewers']
1344 )
1345 )
1346
1347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348def FindCodereviewSettingsFile(filename='codereview.settings'):
1349 """Finds the given file starting in the cwd and going up.
1350
1351 Only looks up to the top of the repository unless an
1352 'inherit-review-settings-ok' file exists in the root of the repository.
1353 """
1354 inherit_ok_file = 'inherit-review-settings-ok'
1355 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001356 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1358 root = '/'
1359 while True:
1360 if filename in os.listdir(cwd):
1361 if os.path.isfile(os.path.join(cwd, filename)):
1362 return open(os.path.join(cwd, filename))
1363 if cwd == root:
1364 break
1365 cwd = os.path.dirname(cwd)
1366
1367
1368def LoadCodereviewSettingsFromFile(fileobj):
1369 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001370 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 def SetProperty(name, setting, unset_error_ok=False):
1373 fullname = 'rietveld.' + name
1374 if setting in keyvals:
1375 RunGit(['config', fullname, keyvals[setting]])
1376 else:
1377 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1378
1379 SetProperty('server', 'CODE_REVIEW_SERVER')
1380 # Only server setting is required. Other settings can be absent.
1381 # In that case, we ignore errors raised during option deletion attempt.
1382 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001383 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1385 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001386 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001387 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001388 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1389 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001390 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001391 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001392 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001393 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1394 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001396 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001397 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001399 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1400 RunGit(['config', 'gerrit.squash-uploads',
1401 keyvals['GERRIT_SQUASH_UPLOADS']])
1402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1404 #should be of the form
1405 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1406 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1407 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1408 keyvals['ORIGIN_URL_CONFIG']])
1409
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001411def urlretrieve(source, destination):
1412 """urllib is broken for SSL connections via a proxy therefore we
1413 can't use urllib.urlretrieve()."""
1414 with open(destination, 'w') as f:
1415 f.write(urllib2.urlopen(source).read())
1416
1417
ukai@chromium.org712d6102013-11-27 00:52:58 +00001418def hasSheBang(fname):
1419 """Checks fname is a #! script."""
1420 with open(fname) as f:
1421 return f.read(2).startswith('#!')
1422
1423
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001424def DownloadHooks(force):
1425 """downloads hooks
1426
1427 Args:
1428 force: True to update hooks. False to install hooks if not present.
1429 """
1430 if not settings.GetIsGerrit():
1431 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001432 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001433 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1434 if not os.access(dst, os.X_OK):
1435 if os.path.exists(dst):
1436 if not force:
1437 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001438 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001439 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001440 if not hasSheBang(dst):
1441 DieWithError('Not a script: %s\n'
1442 'You need to download from\n%s\n'
1443 'into .git/hooks/commit-msg and '
1444 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001445 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1446 except Exception:
1447 if os.path.exists(dst):
1448 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001449 DieWithError('\nFailed to download hooks.\n'
1450 'You need to download from\n%s\n'
1451 'into .git/hooks/commit-msg and '
1452 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001453
1454
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001455@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001457 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001459 parser.add_option('--activate-update', action='store_true',
1460 help='activate auto-updating [rietveld] section in '
1461 '.git/config')
1462 parser.add_option('--deactivate-update', action='store_true',
1463 help='deactivate auto-updating [rietveld] section in '
1464 '.git/config')
1465 options, args = parser.parse_args(args)
1466
1467 if options.deactivate_update:
1468 RunGit(['config', 'rietveld.autoupdate', 'false'])
1469 return
1470
1471 if options.activate_update:
1472 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1473 return
1474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 if len(args) == 0:
1476 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001477 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 return 0
1479
1480 url = args[0]
1481 if not url.endswith('codereview.settings'):
1482 url = os.path.join(url, 'codereview.settings')
1483
1484 # Load code review settings and download hooks (if available).
1485 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001486 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 return 0
1488
1489
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001490def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001491 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001492 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1493 branch = ShortBranchName(branchref)
1494 _, args = parser.parse_args(args)
1495 if not args:
1496 print("Current base-url:")
1497 return RunGit(['config', 'branch.%s.base-url' % branch],
1498 error_ok=False).strip()
1499 else:
1500 print("Setting base-url to %s" % args[0])
1501 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1502 error_ok=False).strip()
1503
1504
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001505def color_for_status(status):
1506 """Maps a Changelist status to color, for CMDstatus and other tools."""
1507 return {
1508 'unsent': Fore.RED,
1509 'waiting': Fore.BLUE,
1510 'reply': Fore.YELLOW,
1511 'lgtm': Fore.GREEN,
1512 'commit': Fore.MAGENTA,
1513 'closed': Fore.CYAN,
1514 'error': Fore.WHITE,
1515 }.get(status, Fore.WHITE)
1516
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001517def fetch_cl_status(branch, auth_config=None):
1518 """Fetches information for an issue and returns (branch, issue, status)."""
1519 cl = Changelist(branchref=branch, auth_config=auth_config)
1520 url = cl.GetIssueURL()
1521 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001522
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001523 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001524 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001525 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001526
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001527 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001528
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001529def get_cl_statuses(
1530 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001531 """Returns a blocking iterable of (branch, issue, color) for given branches.
1532
1533 If fine_grained is true, this will fetch CL statuses from the server.
1534 Otherwise, simply indicate if there's a matching url for the given branches.
1535
1536 If max_processes is specified, it is used as the maximum number of processes
1537 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1538 spawned.
1539 """
1540 # Silence upload.py otherwise it becomes unwieldly.
1541 upload.verbosity = 0
1542
1543 if fine_grained:
1544 # Process one branch synchronously to work through authentication, then
1545 # spawn processes to process all the other branches in parallel.
1546 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001547 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1548 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001549
1550 branches_to_fetch = branches[1:]
1551 pool = ThreadPool(
1552 min(max_processes, len(branches_to_fetch))
1553 if max_processes is not None
1554 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001555 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001556 yield x
1557 else:
1558 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1559 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001560 cl = Changelist(branchref=b, auth_config=auth_config)
1561 url = cl.GetIssueURL()
1562 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001563
rmistry@google.com2dd99862015-06-22 12:22:18 +00001564
1565def upload_branch_deps(cl, args):
1566 """Uploads CLs of local branches that are dependents of the current branch.
1567
1568 If the local branch dependency tree looks like:
1569 test1 -> test2.1 -> test3.1
1570 -> test3.2
1571 -> test2.2 -> test3.3
1572
1573 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1574 run on the dependent branches in this order:
1575 test2.1, test3.1, test3.2, test2.2, test3.3
1576
1577 Note: This function does not rebase your local dependent branches. Use it when
1578 you make a change to the parent branch that will not conflict with its
1579 dependent branches, and you would like their dependencies updated in
1580 Rietveld.
1581 """
1582 if git_common.is_dirty_git_tree('upload-branch-deps'):
1583 return 1
1584
1585 root_branch = cl.GetBranch()
1586 if root_branch is None:
1587 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1588 'Get on a branch!')
1589 if not cl.GetIssue() or not cl.GetPatchset():
1590 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1591 'patchset dependencies without an uploaded CL.')
1592
1593 branches = RunGit(['for-each-ref',
1594 '--format=%(refname:short) %(upstream:short)',
1595 'refs/heads'])
1596 if not branches:
1597 print('No local branches found.')
1598 return 0
1599
1600 # Create a dictionary of all local branches to the branches that are dependent
1601 # on it.
1602 tracked_to_dependents = collections.defaultdict(list)
1603 for b in branches.splitlines():
1604 tokens = b.split()
1605 if len(tokens) == 2:
1606 branch_name, tracked = tokens
1607 tracked_to_dependents[tracked].append(branch_name)
1608
1609 print
1610 print 'The dependent local branches of %s are:' % root_branch
1611 dependents = []
1612 def traverse_dependents_preorder(branch, padding=''):
1613 dependents_to_process = tracked_to_dependents.get(branch, [])
1614 padding += ' '
1615 for dependent in dependents_to_process:
1616 print '%s%s' % (padding, dependent)
1617 dependents.append(dependent)
1618 traverse_dependents_preorder(dependent, padding)
1619 traverse_dependents_preorder(root_branch)
1620 print
1621
1622 if not dependents:
1623 print 'There are no dependent local branches for %s' % root_branch
1624 return 0
1625
1626 print ('This command will checkout all dependent branches and run '
1627 '"git cl upload".')
1628 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1629
andybons@chromium.org962f9462016-02-03 20:00:42 +00001630 # Add a default patchset title to all upload calls in Rietveld.
1631 if not settings.GetIsGerrit():
1632 args.extend(['-t', 'Updated patchset dependency'])
1633
rmistry@google.com2dd99862015-06-22 12:22:18 +00001634 # Record all dependents that failed to upload.
1635 failures = {}
1636 # Go through all dependents, checkout the branch and upload.
1637 try:
1638 for dependent_branch in dependents:
1639 print
1640 print '--------------------------------------'
1641 print 'Running "git cl upload" from %s:' % dependent_branch
1642 RunGit(['checkout', '-q', dependent_branch])
1643 print
1644 try:
1645 if CMDupload(OptionParser(), args) != 0:
1646 print 'Upload failed for %s!' % dependent_branch
1647 failures[dependent_branch] = 1
1648 except: # pylint: disable=W0702
1649 failures[dependent_branch] = 1
1650 print
1651 finally:
1652 # Swap back to the original root branch.
1653 RunGit(['checkout', '-q', root_branch])
1654
1655 print
1656 print 'Upload complete for dependent branches!'
1657 for dependent_branch in dependents:
1658 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1659 print ' %s : %s' % (dependent_branch, upload_status)
1660 print
1661
1662 return 0
1663
1664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001666 """Show status of changelists.
1667
1668 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001669 - Red not sent for review or broken
1670 - Blue waiting for review
1671 - Yellow waiting for you to reply to review
1672 - Green LGTM'ed
1673 - Magenta in the commit queue
1674 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001675
1676 Also see 'git cl comments'.
1677 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 parser.add_option('--field',
1679 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001680 parser.add_option('-f', '--fast', action='store_true',
1681 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001682 parser.add_option(
1683 '-j', '--maxjobs', action='store', type=int,
1684 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001685
1686 auth.add_auth_options(parser)
1687 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001688 if args:
1689 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001690 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001693 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694 if options.field.startswith('desc'):
1695 print cl.GetDescription()
1696 elif options.field == 'id':
1697 issueid = cl.GetIssue()
1698 if issueid:
1699 print issueid
1700 elif options.field == 'patch':
1701 patchset = cl.GetPatchset()
1702 if patchset:
1703 print patchset
1704 elif options.field == 'url':
1705 url = cl.GetIssueURL()
1706 if url:
1707 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001708 return 0
1709
1710 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1711 if not branches:
1712 print('No local branch found.')
1713 return 0
1714
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001715 changes = (
1716 Changelist(branchref=b, auth_config=auth_config)
1717 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001718 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001719 alignment = max(5, max(len(b) for b in branches))
1720 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001721 output = get_cl_statuses(branches,
1722 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001723 max_processes=options.maxjobs,
1724 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001725
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001726 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001727 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001728 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001729 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001730 b, i, status = output.next()
1731 branch_statuses[b] = (i, status)
1732 issue_url, status = branch_statuses.pop(branch)
1733 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001734 reset = Fore.RESET
1735 if not sys.stdout.isatty():
1736 color = ''
1737 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001738 status_str = '(%s)' % status if status else ''
1739 print ' %*s : %s%s %s%s' % (
1740 alignment, ShortBranchName(branch), color, issue_url, status_str,
1741 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001742
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001743 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001744 print
1745 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001746 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001747 if not cl.GetIssue():
1748 print 'No issue assigned.'
1749 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001750 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001751 if not options.fast:
1752 print 'Issue description:'
1753 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754 return 0
1755
1756
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001757def colorize_CMDstatus_doc():
1758 """To be called once in main() to add colors to git cl status help."""
1759 colors = [i for i in dir(Fore) if i[0].isupper()]
1760
1761 def colorize_line(line):
1762 for color in colors:
1763 if color in line.upper():
1764 # Extract whitespaces first and the leading '-'.
1765 indent = len(line) - len(line.lstrip(' ')) + 1
1766 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1767 return line
1768
1769 lines = CMDstatus.__doc__.splitlines()
1770 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1771
1772
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001773@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001775 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776
1777 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001778 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001779 parser.add_option('-r', '--reverse', action='store_true',
1780 help='Lookup the branch(es) for the specified issues. If '
1781 'no issues are specified, all branches with mapped '
1782 'issues will be listed.')
1783 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784
dnj@chromium.org406c4402015-03-03 17:22:28 +00001785 if options.reverse:
1786 branches = RunGit(['for-each-ref', 'refs/heads',
1787 '--format=%(refname:short)']).splitlines()
1788
1789 # Reverse issue lookup.
1790 issue_branch_map = {}
1791 for branch in branches:
1792 cl = Changelist(branchref=branch)
1793 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1794 if not args:
1795 args = sorted(issue_branch_map.iterkeys())
1796 for issue in args:
1797 if not issue:
1798 continue
1799 print 'Branch for issue number %s: %s' % (
1800 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1801 else:
1802 cl = Changelist()
1803 if len(args) > 0:
1804 try:
1805 issue = int(args[0])
1806 except ValueError:
1807 DieWithError('Pass a number to set the issue or none to list it.\n'
1808 'Maybe you want to run git cl status?')
1809 cl.SetIssue(issue)
1810 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811 return 0
1812
1813
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001814def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001815 """Shows or posts review comments for any changelist."""
1816 parser.add_option('-a', '--add-comment', dest='comment',
1817 help='comment to add to an issue')
1818 parser.add_option('-i', dest='issue',
1819 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001820 parser.add_option('-j', '--json-file',
1821 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001822 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001823 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001824 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001825
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001826 issue = None
1827 if options.issue:
1828 try:
1829 issue = int(options.issue)
1830 except ValueError:
1831 DieWithError('A review issue id is expected to be a number')
1832
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001833 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001834
1835 if options.comment:
1836 cl.AddComment(options.comment)
1837 return 0
1838
1839 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001840 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001841 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001842 summary.append({
1843 'date': message['date'],
1844 'lgtm': False,
1845 'message': message['text'],
1846 'not_lgtm': False,
1847 'sender': message['sender'],
1848 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001849 if message['disapproval']:
1850 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001851 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001852 elif message['approval']:
1853 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001854 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001855 elif message['sender'] == data['owner_email']:
1856 color = Fore.MAGENTA
1857 else:
1858 color = Fore.BLUE
1859 print '\n%s%s %s%s' % (
1860 color, message['date'].split('.', 1)[0], message['sender'],
1861 Fore.RESET)
1862 if message['text'].strip():
1863 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001864 if options.json_file:
1865 with open(options.json_file, 'wb') as f:
1866 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001867 return 0
1868
1869
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001870def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001871 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001872 parser.add_option('-d', '--display', action='store_true',
1873 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001874 auth.add_auth_options(parser)
1875 options, _ = parser.parse_args(args)
1876 auth_config = auth.extract_auth_config_from_options(options)
1877 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001878 if not cl.GetIssue():
1879 DieWithError('This branch has no associated changelist.')
1880 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001881 if options.display:
1882 print description.description
1883 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001884 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001885 if cl.GetDescription() != description.description:
1886 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001887 return 0
1888
1889
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890def CreateDescriptionFromLog(args):
1891 """Pulls out the commit log to use as a base for the CL description."""
1892 log_args = []
1893 if len(args) == 1 and not args[0].endswith('.'):
1894 log_args = [args[0] + '..']
1895 elif len(args) == 1 and args[0].endswith('...'):
1896 log_args = [args[0][:-1]]
1897 elif len(args) == 2:
1898 log_args = [args[0] + '..' + args[1]]
1899 else:
1900 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001901 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902
1903
thestig@chromium.org44202a22014-03-11 19:22:18 +00001904def CMDlint(parser, args):
1905 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001906 parser.add_option('--filter', action='append', metavar='-x,+y',
1907 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001908 auth.add_auth_options(parser)
1909 options, args = parser.parse_args(args)
1910 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001911
1912 # Access to a protected member _XX of a client class
1913 # pylint: disable=W0212
1914 try:
1915 import cpplint
1916 import cpplint_chromium
1917 except ImportError:
1918 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1919 return 1
1920
1921 # Change the current working directory before calling lint so that it
1922 # shows the correct base.
1923 previous_cwd = os.getcwd()
1924 os.chdir(settings.GetRoot())
1925 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001926 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001927 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1928 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001929 if not files:
1930 print "Cannot lint an empty CL"
1931 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001932
1933 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001934 command = args + files
1935 if options.filter:
1936 command = ['--filter=' + ','.join(options.filter)] + command
1937 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001938
1939 white_regex = re.compile(settings.GetLintRegex())
1940 black_regex = re.compile(settings.GetLintIgnoreRegex())
1941 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1942 for filename in filenames:
1943 if white_regex.match(filename):
1944 if black_regex.match(filename):
1945 print "Ignoring file %s" % filename
1946 else:
1947 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1948 extra_check_functions)
1949 else:
1950 print "Skipping file %s" % filename
1951 finally:
1952 os.chdir(previous_cwd)
1953 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1954 if cpplint._cpplint_state.error_count != 0:
1955 return 1
1956 return 0
1957
1958
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001960 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001961 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001963 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001964 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001965 auth.add_auth_options(parser)
1966 options, args = parser.parse_args(args)
1967 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968
sbc@chromium.org71437c02015-04-09 19:29:40 +00001969 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001970 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971 return 1
1972
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001973 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 if args:
1975 base_branch = args[0]
1976 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001977 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001978 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001980 cl.RunHook(
1981 committing=not options.upload,
1982 may_prompt=False,
1983 verbose=options.verbose,
1984 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001985 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986
1987
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001988def AddChangeIdToCommitMessage(options, args):
1989 """Re-commits using the current message, assumes the commit hook is in
1990 place.
1991 """
1992 log_desc = options.message or CreateDescriptionFromLog(args)
1993 git_command = ['commit', '--amend', '-m', log_desc]
1994 RunGit(git_command)
1995 new_log_desc = CreateDescriptionFromLog(args)
1996 if CHANGE_ID in new_log_desc:
1997 print 'git-cl: Added Change-Id to commit message.'
1998 else:
1999 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2000
2001
piman@chromium.org336f9122014-09-04 02:16:55 +00002002def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002003 """upload the current branch to gerrit."""
2004 # We assume the remote called "origin" is the one we want.
2005 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002006 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002007
2008 remote, remote_branch = cl.GetRemoteBranch()
2009 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2010 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002012 change_desc = ChangeDescription(
2013 options.message or CreateDescriptionFromLog(args))
2014 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002015 print "\nDescription is empty. Aborting..."
2016 return 1
2017
2018 if options.title:
2019 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002021
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002022 if options.squash:
2023 # Try to get the message from a previous upload.
2024 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
2025 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
2026 if not message:
2027 if not options.force:
2028 change_desc.prompt()
2029
2030 if CHANGE_ID not in change_desc.description:
2031 # Run the commit-msg hook without modifying the head commit by writing
2032 # the commit message to a temporary file and running the hook over it,
2033 # then reading the file back in.
2034 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2035 'commit-msg')
2036 file_handle, msg_file = tempfile.mkstemp(text=True,
2037 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002038 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002039 try:
2040 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002041 try:
2042 fileobj = os.fdopen(file_handle, 'w')
2043 except OSError:
2044 # if fdopen fails, file_handle remains open.
2045 # See https://docs.python.org/2/library/os.html#os.fdopen.
2046 os.close(file_handle)
2047 raise
2048 with fileobj:
2049 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002050 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002051 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002052 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002053 RunCommand([commit_msg_hook, msg_file])
2054 change_desc.set_description(gclient_utils.FileRead(msg_file))
2055 finally:
2056 os.remove(msg_file)
2057
2058 if not change_desc.description:
2059 print "Description is empty; aborting."
2060 return 1
2061
2062 message = change_desc.description
2063
2064 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2065 if remote is '.':
2066 # If our upstream branch is local, we base our squashed commit on its
2067 # squashed version.
2068 parent = ('refs/heads/git_cl_uploads/' +
2069 scm.GIT.ShortBranchName(upstream_branch))
2070
2071 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2072 # will create additional CLs when uploading.
2073 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2074 RunGitSilent(['rev-parse', parent + ':'])):
2075 print 'Upload upstream branch ' + upstream_branch + ' first.'
2076 return 1
2077 else:
2078 parent = cl.GetCommonAncestorWithUpstream()
2079
2080 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2081 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2082 '-m', message]).strip()
2083 else:
2084 if CHANGE_ID not in change_desc.description:
2085 AddChangeIdToCommitMessage(options, args)
2086 ref_to_push = 'HEAD'
2087 parent = '%s/%s' % (gerrit_remote, branch)
2088
2089 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2090 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002091 if len(commits) > 1:
2092 print('WARNING: This will upload %d commits. Run the following command '
2093 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002094 print('git log %s..%s' % (parent, ref_to_push))
2095 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002096 'commit.')
2097 ask_for_data('About to upload; enter to confirm.')
2098
piman@chromium.org336f9122014-09-04 02:16:55 +00002099 if options.reviewers or options.tbr_owners:
2100 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101
ukai@chromium.orge8077812012-02-03 03:41:46 +00002102 receive_options = []
2103 cc = cl.GetCCList().split(',')
2104 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002105 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002106 cc = filter(None, cc)
2107 if cc:
2108 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002109 if change_desc.get_reviewers():
2110 receive_options.extend(
2111 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112
ukai@chromium.orge8077812012-02-03 03:41:46 +00002113 git_command = ['push']
2114 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002115 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002116 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002117 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002118 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002119
2120 if options.squash:
2121 head = RunGit(['rev-parse', 'HEAD']).strip()
2122 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2123
ukai@chromium.orge8077812012-02-03 03:41:46 +00002124 # TODO(ukai): parse Change-Id: and set issue number?
2125 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127
wittman@chromium.org455dc922015-01-26 20:15:50 +00002128def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2129 """Computes the remote branch ref to use for the CL.
2130
2131 Args:
2132 remote (str): The git remote for the CL.
2133 remote_branch (str): The git remote branch for the CL.
2134 target_branch (str): The target branch specified by the user.
2135 pending_prefix (str): The pending prefix from the settings.
2136 """
2137 if not (remote and remote_branch):
2138 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002139
wittman@chromium.org455dc922015-01-26 20:15:50 +00002140 if target_branch:
2141 # Cannonicalize branch references to the equivalent local full symbolic
2142 # refs, which are then translated into the remote full symbolic refs
2143 # below.
2144 if '/' not in target_branch:
2145 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2146 else:
2147 prefix_replacements = (
2148 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2149 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2150 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2151 )
2152 match = None
2153 for regex, replacement in prefix_replacements:
2154 match = re.search(regex, target_branch)
2155 if match:
2156 remote_branch = target_branch.replace(match.group(0), replacement)
2157 break
2158 if not match:
2159 # This is a branch path but not one we recognize; use as-is.
2160 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002161 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2162 # Handle the refs that need to land in different refs.
2163 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002164
wittman@chromium.org455dc922015-01-26 20:15:50 +00002165 # Create the true path to the remote branch.
2166 # Does the following translation:
2167 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2168 # * refs/remotes/origin/master -> refs/heads/master
2169 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2170 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2171 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2172 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2173 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2174 'refs/heads/')
2175 elif remote_branch.startswith('refs/remotes/branch-heads'):
2176 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2177 # If a pending prefix exists then replace refs/ with it.
2178 if pending_prefix:
2179 remote_branch = remote_branch.replace('refs/', pending_prefix)
2180 return remote_branch
2181
2182
piman@chromium.org336f9122014-09-04 02:16:55 +00002183def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002184 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002185 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2186 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002187 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 if options.emulate_svn_auto_props:
2189 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
2191 change_desc = None
2192
pgervais@chromium.org91141372014-01-09 23:27:20 +00002193 if options.email is not None:
2194 upload_args.extend(['--email', options.email])
2195
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002196 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002197 if options.title:
2198 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002199 if options.message:
2200 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002201 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002202 print ("This branch is associated with issue %s. "
2203 "Adding patch to that issue." % cl.GetIssue())
2204 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002205 if options.title:
2206 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002207 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002208 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002209 if options.reviewers or options.tbr_owners:
2210 change_desc.update_reviewers(options.reviewers,
2211 options.tbr_owners,
2212 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002213 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002214 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002215
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002216 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217 print "Description is empty; aborting."
2218 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002219
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002220 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002221 if change_desc.get_reviewers():
2222 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002223 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002224 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002225 DieWithError("Must specify reviewers to send email.")
2226 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002227
2228 # We check this before applying rietveld.private assuming that in
2229 # rietveld.cc only addresses which we can send private CLs to are listed
2230 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2231 # --private is specified explicitly on the command line.
2232 if options.private:
2233 logging.warn('rietveld.cc is ignored since private flag is specified. '
2234 'You need to review and add them manually if necessary.')
2235 cc = cl.GetCCListWithoutDefault()
2236 else:
2237 cc = cl.GetCCList()
2238 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002239 if cc:
2240 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002241
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002242 if options.private or settings.GetDefaultPrivateFlag() == "True":
2243 upload_args.append('--private')
2244
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002245 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002246 if not options.find_copies:
2247 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002249 # Include the upstream repo's URL in the change -- this is useful for
2250 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002251 remote_url = cl.GetGitBaseUrlFromConfig()
2252 if not remote_url:
2253 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002254 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002255 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002256 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2257 remote_url = (cl.GetRemoteUrl() + '@'
2258 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259 if remote_url:
2260 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002261 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002262 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2263 settings.GetPendingRefPrefix())
2264 if target_ref:
2265 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002266
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002267 # Look for dependent patchsets. See crbug.com/480453 for more details.
2268 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2269 upstream_branch = ShortBranchName(upstream_branch)
2270 if remote is '.':
2271 # A local branch is being tracked.
2272 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002273 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002274 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002275 print ('Skipping dependency patchset upload because git config '
2276 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002277 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002278 else:
2279 auth_config = auth.extract_auth_config_from_options(options)
2280 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2281 branch_cl_issue_url = branch_cl.GetIssueURL()
2282 branch_cl_issue = branch_cl.GetIssue()
2283 branch_cl_patchset = branch_cl.GetPatchset()
2284 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2285 upload_args.extend(
2286 ['--depends_on_patchset', '%s:%s' % (
2287 branch_cl_issue, branch_cl_patchset)])
2288 print
2289 print ('The current branch (%s) is tracking a local branch (%s) with '
2290 'an associated CL.') % (cl.GetBranch(), local_branch)
2291 print 'Adding %s/#ps%s as a dependency patchset.' % (
2292 branch_cl_issue_url, branch_cl_patchset)
2293 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002294
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002295 project = settings.GetProject()
2296 if project:
2297 upload_args.extend(['--project', project])
2298
rmistry@google.comef966222015-04-07 11:15:01 +00002299 if options.cq_dry_run:
2300 upload_args.extend(['--cq_dry_run'])
2301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002302 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002303 upload_args = ['upload'] + upload_args + args
2304 logging.info('upload.RealMain(%s)', upload_args)
2305 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002306 issue = int(issue)
2307 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002308 except KeyboardInterrupt:
2309 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310 except:
2311 # If we got an exception after the user typed a description for their
2312 # change, back up the description before re-raising.
2313 if change_desc:
2314 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2315 print '\nGot exception while uploading -- saving description to %s\n' \
2316 % backup_path
2317 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002318 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002319 backup_file.close()
2320 raise
2321
2322 if not cl.GetIssue():
2323 cl.SetIssue(issue)
2324 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002325
2326 if options.use_commit_queue:
2327 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 return 0
2329
2330
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002331def cleanup_list(l):
2332 """Fixes a list so that comma separated items are put as individual items.
2333
2334 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2335 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2336 """
2337 items = sum((i.split(',') for i in l), [])
2338 stripped_items = (i.strip() for i in items)
2339 return sorted(filter(None, stripped_items))
2340
2341
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002342@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002343def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002344 """Uploads the current changelist to codereview.
2345
2346 Can skip dependency patchset uploads for a branch by running:
2347 git config branch.branch_name.skip-deps-uploads True
2348 To unset run:
2349 git config --unset branch.branch_name.skip-deps-uploads
2350 Can also set the above globally by using the --global flag.
2351 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002352 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2353 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002354 parser.add_option('--bypass-watchlists', action='store_true',
2355 dest='bypass_watchlists',
2356 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002357 parser.add_option('-f', action='store_true', dest='force',
2358 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002359 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002360 parser.add_option('-t', dest='title',
2361 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002362 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002363 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002364 help='reviewer email addresses')
2365 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002366 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002367 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002368 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002369 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002370 parser.add_option('--emulate_svn_auto_props',
2371 '--emulate-svn-auto-props',
2372 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002373 dest="emulate_svn_auto_props",
2374 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002375 parser.add_option('-c', '--use-commit-queue', action='store_true',
2376 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002377 parser.add_option('--private', action='store_true',
2378 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002379 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002380 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002381 metavar='TARGET',
2382 help='Apply CL to remote ref TARGET. ' +
2383 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002384 parser.add_option('--squash', action='store_true',
2385 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002386 parser.add_option('--no-squash', action='store_true',
2387 help='Don\'t squash multiple commits into one ' +
2388 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002389 parser.add_option('--email', default=None,
2390 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002391 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2392 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002393 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2394 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002395 help='Send the patchset to do a CQ dry run right after '
2396 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002397 parser.add_option('--dependencies', action='store_true',
2398 help='Uploads CLs of all the local branches that depend on '
2399 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002400
rmistry@google.com2dd99862015-06-22 12:22:18 +00002401 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002402 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002403 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002404 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002405 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002406
sbc@chromium.org71437c02015-04-09 19:29:40 +00002407 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002408 return 1
2409
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002410 options.reviewers = cleanup_list(options.reviewers)
2411 options.cc = cleanup_list(options.cc)
2412
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002413 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002414 if args:
2415 # TODO(ukai): is it ok for gerrit case?
2416 base_branch = args[0]
2417 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002418 if cl.GetBranch() is None:
2419 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2420
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002421 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002422 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002423 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002424
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002425 # Make sure authenticated to Rietveld before running expensive hooks. It is
2426 # a fast, best efforts check. Rietveld still can reject the authentication
2427 # during the actual upload.
2428 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2429 authenticator = auth.get_authenticator_for_host(
2430 cl.GetRietveldServer(), auth_config)
2431 if not authenticator.has_cached_credentials():
2432 raise auth.LoginRequiredError(cl.GetRietveldServer())
2433
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002434 # Apply watchlists on upload.
2435 change = cl.GetChange(base_branch, None)
2436 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2437 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002438 if not options.bypass_watchlists:
2439 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002440
ukai@chromium.orge8077812012-02-03 03:41:46 +00002441 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002442 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002443 # Set the reviewer list now so that presubmit checks can access it.
2444 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002445 change_description.update_reviewers(options.reviewers,
2446 options.tbr_owners,
2447 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002448 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002449 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002450 may_prompt=not options.force,
2451 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002452 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002453 if not hook_results.should_continue():
2454 return 1
2455 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002456 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002457
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002458 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002459 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002460 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002461 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002462 print ('The last upload made from this repository was patchset #%d but '
2463 'the most recent patchset on the server is #%d.'
2464 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002465 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2466 'from another machine or branch the patch you\'re uploading now '
2467 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002468 ask_for_data('About to upload; enter to confirm.')
2469
iannucci@chromium.org79540052012-10-19 23:15:26 +00002470 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002471 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002472 if options.squash and options.no_squash:
2473 DieWithError('Can only use one of --squash or --no-squash')
2474
2475 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2476 not options.no_squash)
2477
piman@chromium.org336f9122014-09-04 02:16:55 +00002478 return GerritUpload(options, args, cl, change)
2479 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002480 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002481 git_set_branch_value('last-upload-hash',
2482 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002483 # Run post upload hooks, if specified.
2484 if settings.GetRunPostUploadHook():
2485 presubmit_support.DoPostUploadExecuter(
2486 change,
2487 cl,
2488 settings.GetRoot(),
2489 options.verbose,
2490 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002491
rmistry@google.com2dd99862015-06-22 12:22:18 +00002492 # Upload all dependencies if specified.
2493 if options.dependencies:
2494 print
2495 print '--dependencies has been specified.'
2496 print 'All dependent local branches will be re-uploaded.'
2497 print
2498 # Remove the dependencies flag from args so that we do not end up in a
2499 # loop.
2500 orig_args.remove('--dependencies')
2501 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002502 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002503
2504
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002505def IsSubmoduleMergeCommit(ref):
2506 # When submodules are added to the repo, we expect there to be a single
2507 # non-git-svn merge commit at remote HEAD with a signature comment.
2508 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002509 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002510 return RunGit(cmd) != ''
2511
2512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002513def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002514 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002516 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002517 Updates changelog with metadata (e.g. pointer to review).
2518 Pushes/dcommits the code upstream.
2519 Updates review and closes.
2520 """
2521 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2522 help='bypass upload presubmit hook')
2523 parser.add_option('-m', dest='message',
2524 help="override review description")
2525 parser.add_option('-f', action='store_true', dest='force',
2526 help="force yes to questions (don't prompt)")
2527 parser.add_option('-c', dest='contributor',
2528 help="external contributor for patch (appended to " +
2529 "description and used as author for git). Should be " +
2530 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002531 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002532 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002533 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002534 auth_config = auth.extract_auth_config_from_options(options)
2535
2536 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002538 current = cl.GetBranch()
2539 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2540 if not settings.GetIsGitSvn() and remote == '.':
2541 print
2542 print 'Attempting to push branch %r into another local branch!' % current
2543 print
2544 print 'Either reparent this branch on top of origin/master:'
2545 print ' git reparent-branch --root'
2546 print
2547 print 'OR run `git rebase-update` if you think the parent branch is already'
2548 print 'committed.'
2549 print
2550 print ' Current parent: %r' % upstream_branch
2551 return 1
2552
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002553 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002554 # Default to merging against our best guess of the upstream branch.
2555 args = [cl.GetUpstreamBranch()]
2556
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002557 if options.contributor:
2558 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2559 print "Please provide contibutor as 'First Last <email@example.com>'"
2560 return 1
2561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002563 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002564
sbc@chromium.org71437c02015-04-09 19:29:40 +00002565 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 return 1
2567
2568 # This rev-list syntax means "show all commits not in my branch that
2569 # are in base_branch".
2570 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2571 base_branch]).splitlines()
2572 if upstream_commits:
2573 print ('Base branch "%s" has %d commits '
2574 'not in this branch.' % (base_branch, len(upstream_commits)))
2575 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2576 return 1
2577
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002578 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002579 svn_head = None
2580 if cmd == 'dcommit' or base_has_submodules:
2581 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2582 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002584 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002585 # If the base_head is a submodule merge commit, the first parent of the
2586 # base_head should be a git-svn commit, which is what we're interested in.
2587 base_svn_head = base_branch
2588 if base_has_submodules:
2589 base_svn_head += '^1'
2590
2591 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592 if extra_commits:
2593 print ('This branch has %d additional commits not upstreamed yet.'
2594 % len(extra_commits.splitlines()))
2595 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2596 'before attempting to %s.' % (base_branch, cmd))
2597 return 1
2598
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002599 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002600 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002601 author = None
2602 if options.contributor:
2603 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002604 hook_results = cl.RunHook(
2605 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002606 may_prompt=not options.force,
2607 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002608 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002609 if not hook_results.should_continue():
2610 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002611
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002612 # Check the tree status if the tree status URL is set.
2613 status = GetTreeStatus()
2614 if 'closed' == status:
2615 print('The tree is closed. Please wait for it to reopen. Use '
2616 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2617 return 1
2618 elif 'unknown' == status:
2619 print('Unable to determine tree status. Please verify manually and '
2620 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2621 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002622 else:
2623 breakpad.SendStack(
2624 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002625 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2626 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002627 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002629 change_desc = ChangeDescription(options.message)
2630 if not change_desc.description and cl.GetIssue():
2631 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002632
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002634 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002635 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002636 else:
2637 print 'No description set.'
2638 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2639 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002641 # Keep a separate copy for the commit message, because the commit message
2642 # contains the link to the Rietveld issue, while the Rietveld message contains
2643 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002644 # Keep a separate copy for the commit message.
2645 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002646 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002648 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002649 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002650 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002651 # after it. Add a period on a new line to circumvent this. Also add a space
2652 # before the period to make sure that Gitiles continues to correctly resolve
2653 # the URL.
2654 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002655 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002656 commit_desc.append_footer('Patch from %s.' % options.contributor)
2657
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002658 print('Description:')
2659 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002660
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002661 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002662 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002663 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002665 # We want to squash all this branch's commits into one commit with the proper
2666 # description. We do this by doing a "reset --soft" to the base branch (which
2667 # keeps the working copy the same), then dcommitting that. If origin/master
2668 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2669 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002670 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002671 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2672 # Delete the branches if they exist.
2673 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2674 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2675 result = RunGitWithCode(showref_cmd)
2676 if result[0] == 0:
2677 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002678
2679 # We might be in a directory that's present in this branch but not in the
2680 # trunk. Move up to the top of the tree so that git commands that expect a
2681 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002682 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002683 if rel_base_path:
2684 os.chdir(rel_base_path)
2685
2686 # Stuff our change into the merge branch.
2687 # We wrap in a try...finally block so if anything goes wrong,
2688 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002689 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002690 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002691 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002692 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002694 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002695 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697 RunGit(
2698 [
2699 'commit', '--author', options.contributor,
2700 '-m', commit_desc.description,
2701 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002703 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002704 if base_has_submodules:
2705 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2706 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2707 RunGit(['checkout', CHERRY_PICK_BRANCH])
2708 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002709 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002710 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002711 pending_prefix = settings.GetPendingRefPrefix()
2712 if not pending_prefix or branch.startswith(pending_prefix):
2713 # If not using refs/pending/heads/* at all, or target ref is already set
2714 # to pending, then push to the target ref directly.
2715 retcode, output = RunGitWithCode(
2716 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002717 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002718 else:
2719 # Cherry-pick the change on top of pending ref and then push it.
2720 assert branch.startswith('refs/'), branch
2721 assert pending_prefix[-1] == '/', pending_prefix
2722 pending_ref = pending_prefix + branch[len('refs/'):]
2723 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002724 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002725 if retcode == 0:
2726 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 else:
2728 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002729 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002730 'svn', 'dcommit',
2731 '-C%s' % options.similarity,
2732 '--no-rebase', '--rmdir',
2733 ]
2734 if settings.GetForceHttpsCommitUrl():
2735 # Allow forcing https commit URLs for some projects that don't allow
2736 # committing to http URLs (like Google Code).
2737 remote_url = cl.GetGitSvnRemoteUrl()
2738 if urlparse.urlparse(remote_url).scheme == 'http':
2739 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002740 cmd_args.append('--commit-url=%s' % remote_url)
2741 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002742 if 'Committed r' in output:
2743 revision = re.match(
2744 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2745 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746 finally:
2747 # And then swap back to the original branch and clean up.
2748 RunGit(['checkout', '-q', cl.GetBranch()])
2749 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002750 if base_has_submodules:
2751 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002753 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002754 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002755 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002756
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002757 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002758 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002759 try:
2760 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2761 # We set pushed_to_pending to False, since it made it all the way to the
2762 # real ref.
2763 pushed_to_pending = False
2764 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002765 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002767 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002768 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002769 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002770 if not to_pending:
2771 if viewvc_url and revision:
2772 change_desc.append_footer(
2773 'Committed: %s%s' % (viewvc_url, revision))
2774 elif revision:
2775 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776 print ('Closing issue '
2777 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002778 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002780 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002781 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002782 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002783 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002784 if options.bypass_hooks:
2785 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2786 else:
2787 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002788 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002789 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002790
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002791 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002792 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2793 print 'The commit is in the pending queue (%s).' % pending_ref
2794 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002795 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002796 'footer.' % branch)
2797
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002798 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2799 if os.path.isfile(hook):
2800 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002801
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002802 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803
2804
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002805def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2806 print
2807 print 'Waiting for commit to be landed on %s...' % real_ref
2808 print '(If you are impatient, you may Ctrl-C once without harm)'
2809 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2810 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2811
2812 loop = 0
2813 while True:
2814 sys.stdout.write('fetching (%d)... \r' % loop)
2815 sys.stdout.flush()
2816 loop += 1
2817
2818 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2819 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2820 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2821 for commit in commits.splitlines():
2822 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2823 print 'Found commit on %s' % real_ref
2824 return commit
2825
2826 current_rev = to_rev
2827
2828
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002829def PushToGitPending(remote, pending_ref, upstream_ref):
2830 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2831
2832 Returns:
2833 (retcode of last operation, output log of last operation).
2834 """
2835 assert pending_ref.startswith('refs/'), pending_ref
2836 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2837 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2838 code = 0
2839 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002840 max_attempts = 3
2841 attempts_left = max_attempts
2842 while attempts_left:
2843 if attempts_left != max_attempts:
2844 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2845 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002846
2847 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002848 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002849 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002850 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002851 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002852 print 'Fetch failed with exit code %d.' % code
2853 if out.strip():
2854 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002855 continue
2856
2857 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002858 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002859 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002860 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002861 if code:
2862 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002863 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2864 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002865 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2866 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002867 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002868 return code, out
2869
2870 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002871 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002872 code, out = RunGitWithCode(
2873 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2874 if code == 0:
2875 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002876 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002877 return code, out
2878
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002879 print 'Push failed with exit code %d.' % code
2880 if out.strip():
2881 print out.strip()
2882 if IsFatalPushFailure(out):
2883 print (
2884 'Fatal push error. Make sure your .netrc credentials and git '
2885 'user.email are correct and you have push access to the repo.')
2886 return code, out
2887
2888 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002889 return code, out
2890
2891
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002892def IsFatalPushFailure(push_stdout):
2893 """True if retrying push won't help."""
2894 return '(prohibited by Gerrit)' in push_stdout
2895
2896
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002897@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002898def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002899 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002900 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002901 if get_footer_svn_id():
2902 # If it looks like previous commits were mirrored with git-svn.
2903 message = """This repository appears to be a git-svn mirror, but no
2904upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2905 else:
2906 message = """This doesn't appear to be an SVN repository.
2907If your project has a true, writeable git repository, you probably want to run
2908'git cl land' instead.
2909If your project has a git mirror of an upstream SVN master, you probably need
2910to run 'git svn init'.
2911
2912Using the wrong command might cause your commit to appear to succeed, and the
2913review to be closed, without actually landing upstream. If you choose to
2914proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002915 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002916 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002917 return SendUpstream(parser, args, 'dcommit')
2918
2919
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002920@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002921def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002922 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002923 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 print('This appears to be an SVN repository.')
2925 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002926 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002927 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002928 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002929
2930
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002931def ParseIssueNum(arg):
2932 """Parses the issue number from args if present otherwise returns None."""
2933 if re.match(r'\d+', arg):
2934 return arg
2935 if arg.startswith('http'):
2936 return re.sub(r'.*/(\d+)/?', r'\1', arg)
2937 return None
2938
2939
2940@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002941def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002942 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943 parser.add_option('-b', dest='newbranch',
2944 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002945 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002947 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2948 help='Change to the directory DIR immediately, '
2949 'before doing anything else.')
2950 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002951 help='failed patches spew .rej files rather than '
2952 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002953 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2954 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002955
2956 group = optparse.OptionGroup(parser,
2957 """Options for continuing work on the current issue uploaded
2958from a different clone (e.g. different machine). Must be used independently from
2959the other options. No issue number should be specified, and the branch must have
2960an issue number associated with it""")
2961 group.add_option('--reapply', action='store_true',
2962 dest='reapply',
2963 help="""Reset the branch and reapply the issue.
2964CAUTION: This will undo any local changes in this branch""")
2965
2966 group.add_option('--pull', action='store_true', dest='pull',
2967 help="Performs a pull before reapplying.")
2968 parser.add_option_group(group)
2969
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002970 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002971 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002972 auth_config = auth.extract_auth_config_from_options(options)
2973
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002974 issue_arg = None
2975 if options.reapply :
2976 if len(args) > 0:
2977 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002978
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002979 cl = Changelist()
2980 issue_arg = cl.GetIssue()
2981 upstream = cl.GetUpstreamBranch()
2982 if upstream == None:
2983 parser.error("No upstream branch specified. Cannot reset branch")
2984
2985 RunGit(['reset', '--hard', upstream])
2986 if options.pull:
2987 RunGit(['pull'])
2988 else:
2989 if len(args) != 1:
2990 parser.error("Must specify issue number")
2991
2992 issue_arg = ParseIssueNum(args[0])
2993
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002994 # The patch URL works because ParseIssueNum won't do any substitution
2995 # as the re.sub pattern fails to match and just returns it.
2996 if issue_arg == None:
2997 parser.print_help()
2998 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003000 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003001 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003002 return 1
3003
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003004 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003005 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003006
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003007 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003008 if options.reapply:
3009 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003010 if options.force:
3011 RunGit(['branch', '-D', options.newbranch],
3012 stderr=subprocess2.PIPE, error_ok=True)
3013 RunGit(['checkout', '-b', options.newbranch,
3014 Changelist().GetUpstreamBranch()])
3015
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003016 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003017 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003018
3019
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003020def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003021 # PatchIssue should never be called with a dirty tree. It is up to the
3022 # caller to check this, but just in case we assert here since the
3023 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003024 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003025
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003026 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003027 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003028 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003029 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003030 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003031 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003032 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003033 # Assume it's a URL to the patch. Default to https.
3034 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003035 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003036 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003037 DieWithError('Must pass an issue ID or full URL for '
3038 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003039 issue = int(match.group(2))
3040 cl = Changelist(issue=issue, auth_config=auth_config)
3041 cl.rietveld_server = match.group(1)
3042 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003043 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 # Switch up to the top-level directory, if necessary, in preparation for
3046 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003047 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003048 if top:
3049 os.chdir(top)
3050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051 # Git patches have a/ at the beginning of source paths. We strip that out
3052 # with a sed script rather than the -p flag to patch so we can feed either
3053 # Git or svn-style patches into the same apply command.
3054 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003055 try:
3056 patch_data = subprocess2.check_output(
3057 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3058 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059 DieWithError('Git patch mungling failed.')
3060 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003061
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003062 # We use "git apply" to apply the patch instead of "patch" so that we can
3063 # pick up file adds.
3064 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003065 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003066 if directory:
3067 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003068 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003069 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003070 elif IsGitVersionAtLeast('1.7.12'):
3071 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003072 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003073 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003074 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003075 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003076 print 'Failed to apply the patch'
3077 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078
3079 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003080 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003081 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3082 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003083 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3084 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003085 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003086 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003087 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003088 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089 else:
3090 print "Patch applied to index."
3091 return 0
3092
3093
3094def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003095 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096 # Provide a wrapper for git svn rebase to help avoid accidental
3097 # git svn dcommit.
3098 # It's the only command that doesn't use parser at all since we just defer
3099 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003100
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003101 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003102
3103
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003104def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105 """Fetches the tree status and returns either 'open', 'closed',
3106 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003107 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003108 if url:
3109 status = urllib2.urlopen(url).read().lower()
3110 if status.find('closed') != -1 or status == '0':
3111 return 'closed'
3112 elif status.find('open') != -1 or status == '1':
3113 return 'open'
3114 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003115 return 'unset'
3116
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003118def GetTreeStatusReason():
3119 """Fetches the tree status from a json url and returns the message
3120 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003121 url = settings.GetTreeStatusUrl()
3122 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123 connection = urllib2.urlopen(json_url)
3124 status = json.loads(connection.read())
3125 connection.close()
3126 return status['message']
3127
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003128
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003129def GetBuilderMaster(bot_list):
3130 """For a given builder, fetch the master from AE if available."""
3131 map_url = 'https://builders-map.appspot.com/'
3132 try:
3133 master_map = json.load(urllib2.urlopen(map_url))
3134 except urllib2.URLError as e:
3135 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3136 (map_url, e))
3137 except ValueError as e:
3138 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3139 if not master_map:
3140 return None, 'Failed to build master map.'
3141
3142 result_master = ''
3143 for bot in bot_list:
3144 builder = bot.split(':', 1)[0]
3145 master_list = master_map.get(builder, [])
3146 if not master_list:
3147 return None, ('No matching master for builder %s.' % builder)
3148 elif len(master_list) > 1:
3149 return None, ('The builder name %s exists in multiple masters %s.' %
3150 (builder, master_list))
3151 else:
3152 cur_master = master_list[0]
3153 if not result_master:
3154 result_master = cur_master
3155 elif result_master != cur_master:
3156 return None, 'The builders do not belong to the same master.'
3157 return result_master, None
3158
3159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003160def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003161 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003162 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003163 status = GetTreeStatus()
3164 if 'unset' == status:
3165 print 'You must configure your tree status URL by running "git cl config".'
3166 return 2
3167
3168 print "The tree is %s" % status
3169 print
3170 print GetTreeStatusReason()
3171 if status != 'open':
3172 return 1
3173 return 0
3174
3175
maruel@chromium.org15192402012-09-06 12:38:29 +00003176def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003177 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003178 group = optparse.OptionGroup(parser, "Try job options")
3179 group.add_option(
3180 "-b", "--bot", action="append",
3181 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3182 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003183 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003184 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003185 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003186 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003187 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003188 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003189 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003190 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003191 "-r", "--revision",
3192 help="Revision to use for the try job; default: the "
3193 "revision will be determined by the try server; see "
3194 "its waterfall for more info")
3195 group.add_option(
3196 "-c", "--clobber", action="store_true", default=False,
3197 help="Force a clobber before building; e.g. don't do an "
3198 "incremental build")
3199 group.add_option(
3200 "--project",
3201 help="Override which project to use. Projects are defined "
3202 "server-side to define what default bot set to use")
3203 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003204 "-p", "--property", dest="properties", action="append", default=[],
3205 help="Specify generic properties in the form -p key1=value1 -p "
3206 "key2=value2 etc (buildbucket only). The value will be treated as "
3207 "json if decodable, or as string otherwise.")
3208 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003209 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003210 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003211 "--use-rietveld", action="store_true", default=False,
3212 help="Use Rietveld to trigger try jobs.")
3213 group.add_option(
3214 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3215 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003216 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003217 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003218 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003219 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003220
machenbach@chromium.org45453142015-09-15 08:45:22 +00003221 if options.use_rietveld and options.properties:
3222 parser.error('Properties can only be specified with buildbucket')
3223
3224 # Make sure that all properties are prop=value pairs.
3225 bad_params = [x for x in options.properties if '=' not in x]
3226 if bad_params:
3227 parser.error('Got properties with missing "=": %s' % bad_params)
3228
maruel@chromium.org15192402012-09-06 12:38:29 +00003229 if args:
3230 parser.error('Unknown arguments: %s' % args)
3231
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003232 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003233 if not cl.GetIssue():
3234 parser.error('Need to upload first')
3235
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003236 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003237 if props.get('closed'):
3238 parser.error('Cannot send tryjobs for a closed CL')
3239
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003240 if props.get('private'):
3241 parser.error('Cannot use trybots with private issue')
3242
maruel@chromium.org15192402012-09-06 12:38:29 +00003243 if not options.name:
3244 options.name = cl.GetBranch()
3245
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003246 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003247 options.master, err_msg = GetBuilderMaster(options.bot)
3248 if err_msg:
3249 parser.error('Tryserver master cannot be found because: %s\n'
3250 'Please manually specify the tryserver master'
3251 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003252
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003253 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003254 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003255 if not options.bot:
3256 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003257
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003258 # Get try masters from PRESUBMIT.py files.
3259 masters = presubmit_support.DoGetTryMasters(
3260 change,
3261 change.LocalPaths(),
3262 settings.GetRoot(),
3263 None,
3264 None,
3265 options.verbose,
3266 sys.stdout)
3267 if masters:
3268 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003269
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003270 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3271 options.bot = presubmit_support.DoGetTrySlaves(
3272 change,
3273 change.LocalPaths(),
3274 settings.GetRoot(),
3275 None,
3276 None,
3277 options.verbose,
3278 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003279
3280 if not options.bot:
3281 # Get try masters from cq.cfg if any.
3282 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3283 # location.
3284 cq_cfg = os.path.join(change.RepositoryRoot(),
3285 'infra', 'config', 'cq.cfg')
3286 if os.path.exists(cq_cfg):
3287 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003288 cq_masters = commit_queue.get_master_builder_map(
3289 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003290 for master, builders in cq_masters.iteritems():
3291 for builder in builders:
3292 # Skip presubmit builders, because these will fail without LGTM.
3293 if 'presubmit' not in builder.lower():
3294 masters.setdefault(master, {})[builder] = ['defaulttests']
3295 if masters:
3296 return masters
3297
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003298 if not options.bot:
3299 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003300
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003301 builders_and_tests = {}
3302 # TODO(machenbach): The old style command-line options don't support
3303 # multiple try masters yet.
3304 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3305 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3306
3307 for bot in old_style:
3308 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003309 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003310 elif ',' in bot:
3311 parser.error('Specify one bot per --bot flag')
3312 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003313 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003314
3315 for bot, tests in new_style:
3316 builders_and_tests.setdefault(bot, []).extend(tests)
3317
3318 # Return a master map with one master to be backwards compatible. The
3319 # master name defaults to an empty string, which will cause the master
3320 # not to be set on rietveld (deprecated).
3321 return {options.master: builders_and_tests}
3322
3323 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003324
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003325 for builders in masters.itervalues():
3326 if any('triggered' in b for b in builders):
3327 print >> sys.stderr, (
3328 'ERROR You are trying to send a job to a triggered bot. This type of'
3329 ' bot requires an\ninitial job from a parent (usually a builder). '
3330 'Instead send your job to the parent.\n'
3331 'Bot list: %s' % builders)
3332 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003333
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003334 patchset = cl.GetMostRecentPatchset()
3335 if patchset and patchset != cl.GetPatchset():
3336 print(
3337 '\nWARNING Mismatch between local config and server. Did a previous '
3338 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3339 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003340 if options.luci:
3341 trigger_luci_job(cl, masters, options)
3342 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003343 try:
3344 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3345 except BuildbucketResponseException as ex:
3346 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003347 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003348 except Exception as e:
3349 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3350 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3351 e, stacktrace)
3352 return 1
3353 else:
3354 try:
3355 cl.RpcServer().trigger_distributed_try_jobs(
3356 cl.GetIssue(), patchset, options.name, options.clobber,
3357 options.revision, masters)
3358 except urllib2.HTTPError as e:
3359 if e.code == 404:
3360 print('404 from rietveld; '
3361 'did you mean to use "git try" instead of "git cl try"?')
3362 return 1
3363 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003364
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003365 for (master, builders) in sorted(masters.iteritems()):
3366 if master:
3367 print 'Master: %s' % master
3368 length = max(len(builder) for builder in builders)
3369 for builder in sorted(builders):
3370 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003371 return 0
3372
3373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003376 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003377 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003378 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003379 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003380
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003382 if args:
3383 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003384 branch = cl.GetBranch()
3385 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003386 cl = Changelist()
3387 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003388
3389 # Clear configured merge-base, if there is one.
3390 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003391 else:
3392 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393 return 0
3394
3395
thestig@chromium.org00858c82013-12-02 23:08:03 +00003396def CMDweb(parser, args):
3397 """Opens the current CL in the web browser."""
3398 _, args = parser.parse_args(args)
3399 if args:
3400 parser.error('Unrecognized args: %s' % ' '.join(args))
3401
3402 issue_url = Changelist().GetIssueURL()
3403 if not issue_url:
3404 print >> sys.stderr, 'ERROR No issue to open'
3405 return 1
3406
3407 webbrowser.open(issue_url)
3408 return 0
3409
3410
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003411def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003412 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003413 auth.add_auth_options(parser)
3414 options, args = parser.parse_args(args)
3415 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003416 if args:
3417 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003418 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003419 props = cl.GetIssueProperties()
3420 if props.get('private'):
3421 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003422 cl.SetFlag('commit', '1')
3423 return 0
3424
3425
groby@chromium.org411034a2013-02-26 15:12:01 +00003426def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003427 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003428 auth.add_auth_options(parser)
3429 options, args = parser.parse_args(args)
3430 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003431 if args:
3432 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003433 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003434 # Ensure there actually is an issue to close.
3435 cl.GetDescription()
3436 cl.CloseIssue()
3437 return 0
3438
3439
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003440def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003441 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003442 auth.add_auth_options(parser)
3443 options, args = parser.parse_args(args)
3444 auth_config = auth.extract_auth_config_from_options(options)
3445 if args:
3446 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003447
3448 # Uncommitted (staged and unstaged) changes will be destroyed by
3449 # "git reset --hard" if there are merging conflicts in PatchIssue().
3450 # Staged changes would be committed along with the patch from last
3451 # upload, hence counted toward the "last upload" side in the final
3452 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003453 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003454 return 1
3455
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003456 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003457 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003458 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003459 if not issue:
3460 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003461 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003462 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003463
3464 # Create a new branch based on the merge-base
3465 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3466 try:
3467 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003468 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003469 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003470 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003471 return rtn
3472
wychen@chromium.org06928532015-02-03 02:11:29 +00003473 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003474 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003475 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003476 finally:
3477 RunGit(['checkout', '-q', branch])
3478 RunGit(['branch', '-D', TMP_BRANCH])
3479
3480 return 0
3481
3482
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003483def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003484 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003485 parser.add_option(
3486 '--no-color',
3487 action='store_true',
3488 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003489 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003490 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003491 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003492
3493 author = RunGit(['config', 'user.email']).strip() or None
3494
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003495 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003496
3497 if args:
3498 if len(args) > 1:
3499 parser.error('Unknown args')
3500 base_branch = args[0]
3501 else:
3502 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003503 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003504
3505 change = cl.GetChange(base_branch, None)
3506 return owners_finder.OwnersFinder(
3507 [f.LocalPath() for f in
3508 cl.GetChange(base_branch, None).AffectedFiles()],
3509 change.RepositoryRoot(), author,
3510 fopen=file, os_path=os.path, glob=glob.glob,
3511 disable_color=options.no_color).run()
3512
3513
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003514def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003515 """Generates a diff command."""
3516 # Generate diff for the current branch's changes.
3517 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3518 upstream_commit, '--' ]
3519
3520 if args:
3521 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003522 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003523 diff_cmd.append(arg)
3524 else:
3525 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003526
3527 return diff_cmd
3528
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003529def MatchingFileType(file_name, extensions):
3530 """Returns true if the file name ends with one of the given extensions."""
3531 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003532
enne@chromium.org555cfe42014-01-29 18:21:39 +00003533@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003534def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003535 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003536 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003537 parser.add_option('--full', action='store_true',
3538 help='Reformat the full content of all touched files')
3539 parser.add_option('--dry-run', action='store_true',
3540 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003541 parser.add_option('--python', action='store_true',
3542 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003543 parser.add_option('--diff', action='store_true',
3544 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003545 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003546
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003547 # git diff generates paths against the root of the repository. Change
3548 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003549 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003550 if rel_base_path:
3551 os.chdir(rel_base_path)
3552
digit@chromium.org29e47272013-05-17 17:01:46 +00003553 # Grab the merge-base commit, i.e. the upstream commit of the current
3554 # branch when it was created or the last time it was rebased. This is
3555 # to cover the case where the user may have called "git fetch origin",
3556 # moving the origin branch to a newer commit, but hasn't rebased yet.
3557 upstream_commit = None
3558 cl = Changelist()
3559 upstream_branch = cl.GetUpstreamBranch()
3560 if upstream_branch:
3561 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3562 upstream_commit = upstream_commit.strip()
3563
3564 if not upstream_commit:
3565 DieWithError('Could not find base commit for this branch. '
3566 'Are you in detached state?')
3567
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003568 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3569 diff_output = RunGit(changed_files_cmd)
3570 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003571 # Filter out files deleted by this CL
3572 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003573
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003574 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3575 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3576 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
digit@chromium.org29e47272013-05-17 17:01:46 +00003577
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003578 top_dir = os.path.normpath(
3579 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3580
3581 # Locate the clang-format binary in the checkout
3582 try:
3583 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3584 except clang_format.NotFoundError, e:
3585 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003586
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003587 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3588 # formatted. This is used to block during the presubmit.
3589 return_value = 0
3590
digit@chromium.org29e47272013-05-17 17:01:46 +00003591 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003592 if clang_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003593 cmd = [clang_format_tool]
3594 if not opts.dry_run and not opts.diff:
3595 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003596 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003597 if opts.diff:
3598 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003599 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003600 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003601 env['PATH'] = str(os.path.dirname(clang_format_tool))
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003602 try:
3603 script = clang_format.FindClangFormatScriptInChromiumTree(
3604 'clang-format-diff.py')
3605 except clang_format.NotFoundError, e:
3606 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003607
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003608 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003609 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003610 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003611
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003612 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3613 diff_output = RunGit(diff_cmd)
3614
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003615 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003616 if opts.diff:
3617 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003618 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003619 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003620
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003621 # Similar code to above, but using yapf on .py files rather than clang-format
3622 # on C/C++ files
3623 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003624 yapf_tool = gclient_utils.FindExecutable('yapf')
3625 if yapf_tool is None:
3626 DieWithError('yapf not found in PATH')
3627
3628 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003629 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003630 cmd = [yapf_tool]
3631 if not opts.dry_run and not opts.diff:
3632 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003633 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003634 if opts.diff:
3635 sys.stdout.write(stdout)
3636 else:
3637 # TODO(sbc): yapf --lines mode still has some issues.
3638 # https://github.com/google/yapf/issues/154
3639 DieWithError('--python currently only works with --full')
3640
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003641 # Dart's formatter does not have the nice property of only operating on
3642 # modified chunks, so hard code full.
3643 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003644 try:
3645 command = [dart_format.FindDartFmtToolInChromiumTree()]
3646 if not opts.dry_run and not opts.diff:
3647 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003648 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003649
3650 stdout = RunCommand(command, cwd=top_dir, env=env)
3651 if opts.dry_run and stdout:
3652 return_value = 2
3653 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003654 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3655 'found in this checkout. Files in other languages are still ' +
3656 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003657
3658 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003659
3660
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003661@subcommand.usage('<codereview url or issue id>')
3662def CMDcheckout(parser, args):
3663 """Checks out a branch associated with a given Rietveld issue."""
3664 _, args = parser.parse_args(args)
3665
3666 if len(args) != 1:
3667 parser.print_help()
3668 return 1
3669
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003670 target_issue = ParseIssueNum(args[0])
3671 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003672 parser.print_help()
3673 return 1
3674
3675 key_and_issues = [x.split() for x in RunGit(
3676 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3677 .splitlines()]
3678 branches = []
3679 for key, issue in key_and_issues:
3680 if issue == target_issue:
3681 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3682
3683 if len(branches) == 0:
3684 print 'No branch found for issue %s.' % target_issue
3685 return 1
3686 if len(branches) == 1:
3687 RunGit(['checkout', branches[0]])
3688 else:
3689 print 'Multiple branches match issue %s:' % target_issue
3690 for i in range(len(branches)):
3691 print '%d: %s' % (i, branches[i])
3692 which = raw_input('Choose by index: ')
3693 try:
3694 RunGit(['checkout', branches[int(which)]])
3695 except (IndexError, ValueError):
3696 print 'Invalid selection, not checking out any branch.'
3697 return 1
3698
3699 return 0
3700
3701
maruel@chromium.org29404b52014-09-08 22:58:00 +00003702def CMDlol(parser, args):
3703 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003704 print zlib.decompress(base64.b64decode(
3705 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3706 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3707 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3708 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003709 return 0
3710
3711
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003712class OptionParser(optparse.OptionParser):
3713 """Creates the option parse and add --verbose support."""
3714 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003715 optparse.OptionParser.__init__(
3716 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003717 self.add_option(
3718 '-v', '--verbose', action='count', default=0,
3719 help='Use 2 times for more debugging info')
3720
3721 def parse_args(self, args=None, values=None):
3722 options, args = optparse.OptionParser.parse_args(self, args, values)
3723 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3724 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3725 return options, args
3726
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003729 if sys.hexversion < 0x02060000:
3730 print >> sys.stderr, (
3731 '\nYour python version %s is unsupported, please upgrade.\n' %
3732 sys.version.split(' ', 1)[0])
3733 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003734
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003735 # Reload settings.
3736 global settings
3737 settings = Settings()
3738
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003739 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003740 dispatcher = subcommand.CommandDispatcher(__name__)
3741 try:
3742 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003743 except auth.AuthenticationError as e:
3744 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003745 except urllib2.HTTPError, e:
3746 if e.code != 500:
3747 raise
3748 DieWithError(
3749 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3750 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003751 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003752
3753
3754if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003755 # These affect sys.stdout so do it outside of main() to simplify mocks in
3756 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003757 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003758 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003759 try:
3760 sys.exit(main(sys.argv[1:]))
3761 except KeyboardInterrupt:
3762 sys.stderr.write('interrupted\n')
3763 sys.exit(1)