blob: 8fa8a30b14e909843b6e348365c0e59eea77f50a [file] [log] [blame]
vapier@chromium.orgb0fe1232015-11-12 05:50:01 +00001#!/usr/bin/env python2.7
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
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000045import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000046import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000047import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000049from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000050import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000053import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000055import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import watchlists
58
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000061DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000062POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000064GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000065CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000066REFS_THAT_ALIAS_TO_OTHER_REFS = {
67 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
68 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
69}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070
thestig@chromium.org44202a22014-03-11 19:22:18 +000071# Valid extensions for files we want to lint.
72DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
73DEFAULT_LINT_IGNORE_REGEX = r"$^"
74
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000075# Shortcut since it quickly becomes redundant.
76Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000077
maruel@chromium.orgddd59412011-11-30 14:20:38 +000078# Initialized in main()
79settings = None
80
81
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000083 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 sys.exit(1)
85
86
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000087def GetNoGitPagerEnv():
88 env = os.environ.copy()
89 # 'cat' is a magical git string that disables pagers on all platforms.
90 env['GIT_PAGER'] = 'cat'
91 return env
92
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000093
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000096 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000097 except subprocess2.CalledProcessError as e:
98 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 'Command "%s" failed.\n%s' % (
102 ' '.join(args), error_message or e.stdout or ''))
103 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104
105
106def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000108 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000111def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000113 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114 if suppress_stderr:
115 stderr = subprocess2.VOID
116 else:
117 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000118 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 stdout=subprocess2.PIPE,
121 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000122 return code, out[0]
123 except ValueError:
124 # When the subprocess fails, it returns None. That triggers a ValueError
125 # when trying to unpack the return value into (out, code).
126 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127
128
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000129def RunGitSilent(args):
130 """Returns stdout, suppresses stderr and ingores the return code."""
131 return RunGitWithCode(args, suppress_stderr=True)[1]
132
133
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000134def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000135 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 return (version.startswith(prefix) and
138 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139
140
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000141def BranchExists(branch):
142 """Return True if specified branch exists."""
143 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
144 suppress_stderr=True)
145 return not code
146
147
maruel@chromium.org90541732011-04-01 17:54:18 +0000148def ask_for_data(prompt):
149 try:
150 return raw_input(prompt)
151 except KeyboardInterrupt:
152 # Hide the exception.
153 sys.exit(1)
154
155
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156def git_set_branch_value(key, value):
157 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000158 if not branch:
159 return
160
161 cmd = ['config']
162 if isinstance(value, int):
163 cmd.append('--int')
164 git_key = 'branch.%s.%s' % (branch, key)
165 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000166
167
168def git_get_branch_default(key, default):
169 branch = Changelist().GetBranch()
170 if branch:
171 git_key = 'branch.%s.%s' % (branch, key)
172 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
173 try:
174 return int(stdout.strip())
175 except ValueError:
176 pass
177 return default
178
179
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000180def add_git_similarity(parser):
181 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000182 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183 help='Sets the percentage that a pair of files need to match in order to'
184 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 parser.add_option(
186 '--find-copies', action='store_true',
187 help='Allows git to look for copies.')
188 parser.add_option(
189 '--no-find-copies', action='store_false', dest='find_copies',
190 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000191
192 old_parser_args = parser.parse_args
193 def Parse(args):
194 options, args = old_parser_args(args)
195
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000197 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 print('Note: Saving similarity of %d%% in git config.'
200 % options.similarity)
201 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 options.similarity = max(0, min(options.similarity, 100))
204
205 if options.find_copies is None:
206 options.find_copies = bool(
207 git_get_branch_default('git-find-copies', True))
208 else:
209 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000210
211 print('Using %d%% similarity for rename/copy detection. '
212 'Override with --similarity.' % options.similarity)
213
214 return options, args
215 parser.parse_args = Parse
216
217
machenbach@chromium.org45453142015-09-15 08:45:22 +0000218def _get_properties_from_options(options):
219 properties = dict(x.split('=', 1) for x in options.properties)
220 for key, val in properties.iteritems():
221 try:
222 properties[key] = json.loads(val)
223 except ValueError:
224 pass # If a value couldn't be evaluated, treat it as a string.
225 return properties
226
227
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000228def _prefix_master(master):
229 """Convert user-specified master name to full master name.
230
231 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
232 name, while the developers always use shortened master name
233 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
234 function does the conversion for buildbucket migration.
235 """
236 prefix = 'master.'
237 if master.startswith(prefix):
238 return master
239 return '%s%s' % (prefix, master)
240
241
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000242def trigger_luci_job(changelist, masters, options):
243 """Send a job to run on LUCI."""
244 issue_props = changelist.GetIssueProperties()
245 issue = changelist.GetIssue()
246 patchset = changelist.GetMostRecentPatchset()
247 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000248 # TODO(hinoka et al): add support for other properties.
249 # Currently, this completely ignores testfilter and other properties.
250 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000251 luci_trigger.trigger(
252 builder, 'HEAD', issue, patchset, issue_props['project'])
253
254
machenbach@chromium.org45453142015-09-15 08:45:22 +0000255def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000256 rietveld_url = settings.GetDefaultServerUrl()
257 rietveld_host = urlparse.urlparse(rietveld_url).hostname
258 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
259 http = authenticator.authorize(httplib2.Http())
260 http.force_exception_to_status_code = True
261 issue_props = changelist.GetIssueProperties()
262 issue = changelist.GetIssue()
263 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000264 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000265
266 buildbucket_put_url = (
267 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000268 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000269 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
270 hostname=rietveld_host,
271 issue=issue,
272 patch=patchset)
273
274 batch_req_body = {'builds': []}
275 print_text = []
276 print_text.append('Tried jobs on:')
277 for master, builders_and_tests in sorted(masters.iteritems()):
278 print_text.append('Master: %s' % master)
279 bucket = _prefix_master(master)
280 for builder, tests in sorted(builders_and_tests.iteritems()):
281 print_text.append(' %s: %s' % (builder, tests))
282 parameters = {
283 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000284 'changes': [{
285 'author': {'email': issue_props['owner_email']},
286 'revision': options.revision,
287 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000288 'properties': {
289 'category': category,
290 'issue': issue,
291 'master': master,
292 'patch_project': issue_props['project'],
293 'patch_storage': 'rietveld',
294 'patchset': patchset,
295 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 },
298 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000299 if tests:
300 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000301 if properties:
302 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000303 if options.clobber:
304 parameters['properties']['clobber'] = True
305 batch_req_body['builds'].append(
306 {
307 'bucket': bucket,
308 'parameters_json': json.dumps(parameters),
309 'tags': ['builder:%s' % builder,
310 'buildset:%s' % buildset,
311 'master:%s' % master,
312 'user_agent:git_cl_try']
313 }
314 )
315
316 for try_count in xrange(3):
317 response, content = http.request(
318 buildbucket_put_url,
319 'PUT',
320 body=json.dumps(batch_req_body),
321 headers={'Content-Type': 'application/json'},
322 )
323 content_json = None
324 try:
325 content_json = json.loads(content)
326 except ValueError:
327 pass
328
329 # Buildbucket could return an error even if status==200.
330 if content_json and content_json.get('error'):
331 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
332 content_json['error'].get('code', ''),
333 content_json['error'].get('reason', ''),
334 content_json['error'].get('message', ''))
335 raise BuildbucketResponseException(msg)
336
337 if response.status == 200:
338 if not content_json:
339 raise BuildbucketResponseException(
340 'Buildbucket returns invalid json content: %s.\n'
341 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
342 content)
343 break
344 if response.status < 500 or try_count >= 2:
345 raise httplib2.HttpLib2Error(content)
346
347 # status >= 500 means transient failures.
348 logging.debug('Transient errors when triggering tryjobs. Will retry.')
349 time.sleep(0.5 + 1.5*try_count)
350
351 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000352
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000354def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
355 """Return the corresponding git ref if |base_url| together with |glob_spec|
356 matches the full |url|.
357
358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
359 """
360 fetch_suburl, as_ref = glob_spec.split(':')
361 if allow_wildcards:
362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
363 if glob_match:
364 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
365 # "branches/{472,597,648}/src:refs/remotes/svn/*".
366 branch_re = re.escape(base_url)
367 if glob_match.group(1):
368 branch_re += '/' + re.escape(glob_match.group(1))
369 wildcard = glob_match.group(2)
370 if wildcard == '*':
371 branch_re += '([^/]*)'
372 else:
373 # Escape and replace surrounding braces with parentheses and commas
374 # with pipe symbols.
375 wildcard = re.escape(wildcard)
376 wildcard = re.sub('^\\\\{', '(', wildcard)
377 wildcard = re.sub('\\\\,', '|', wildcard)
378 wildcard = re.sub('\\\\}$', ')', wildcard)
379 branch_re += wildcard
380 if glob_match.group(3):
381 branch_re += re.escape(glob_match.group(3))
382 match = re.match(branch_re, url)
383 if match:
384 return re.sub('\*$', match.group(1), as_ref)
385
386 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
387 if fetch_suburl:
388 full_url = base_url + '/' + fetch_suburl
389 else:
390 full_url = base_url
391 if full_url == url:
392 return as_ref
393 return None
394
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000395
iannucci@chromium.org79540052012-10-19 23:15:26 +0000396def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000397 """Prints statistics about the change to the user."""
398 # --no-ext-diff is broken in some versions of Git, so try to work around
399 # this by overriding the environment (but there is still a problem if the
400 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000401 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000402 if 'GIT_EXTERNAL_DIFF' in env:
403 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000404
405 if find_copies:
406 similarity_options = ['--find-copies-harder', '-l100000',
407 '-C%s' % similarity]
408 else:
409 similarity_options = ['-M%s' % similarity]
410
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000411 try:
412 stdout = sys.stdout.fileno()
413 except AttributeError:
414 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000415 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000416 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000417 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000418 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000419
420
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000421class BuildbucketResponseException(Exception):
422 pass
423
424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425class Settings(object):
426 def __init__(self):
427 self.default_server = None
428 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000429 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.is_git_svn = None
431 self.svn_branch = None
432 self.tree_status_url = None
433 self.viewvc_url = None
434 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000435 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000437 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000438 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000439 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440
441 def LazyUpdateIfNeeded(self):
442 """Updates the settings from a codereview.settings file, if available."""
443 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000444 # The only value that actually changes the behavior is
445 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000446 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000447 error_ok=True
448 ).strip().lower()
449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000451 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000453 # set updated to True to avoid infinite calling loop
454 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000455 self.updated = True
456 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457 self.updated = True
458
459 def GetDefaultServerUrl(self, error_ok=False):
460 if not self.default_server:
461 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000462 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000463 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 if error_ok:
465 return self.default_server
466 if not self.default_server:
467 error_message = ('Could not find settings file. You must configure '
468 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000469 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000470 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 return self.default_server
472
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000473 @staticmethod
474 def GetRelativeRoot():
475 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000478 if self.root is None:
479 self.root = os.path.abspath(self.GetRelativeRoot())
480 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481
482 def GetIsGitSvn(self):
483 """Return true if this repo looks like it's using git-svn."""
484 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000485 if self.GetPendingRefPrefix():
486 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
487 self.is_git_svn = False
488 else:
489 # If you have any "svn-remote.*" config keys, we think you're using svn.
490 self.is_git_svn = RunGitWithCode(
491 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000492 return self.is_git_svn
493
494 def GetSVNBranch(self):
495 if self.svn_branch is None:
496 if not self.GetIsGitSvn():
497 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
498
499 # Try to figure out which remote branch we're based on.
500 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000501 # 1) iterate through our branch history and find the svn URL.
502 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503
504 # regexp matching the git-svn line that contains the URL.
505 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
506
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000507 # We don't want to go through all of history, so read a line from the
508 # pipe at a time.
509 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000510 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000511 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
512 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000513 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000514 for line in proc.stdout:
515 match = git_svn_re.match(line)
516 if match:
517 url = match.group(1)
518 proc.stdout.close() # Cut pipe.
519 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000520
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000521 if url:
522 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
523 remotes = RunGit(['config', '--get-regexp',
524 r'^svn-remote\..*\.url']).splitlines()
525 for remote in remotes:
526 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000527 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000528 remote = match.group(1)
529 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000530 rewrite_root = RunGit(
531 ['config', 'svn-remote.%s.rewriteRoot' % remote],
532 error_ok=True).strip()
533 if rewrite_root:
534 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000535 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000536 ['config', 'svn-remote.%s.fetch' % remote],
537 error_ok=True).strip()
538 if fetch_spec:
539 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
540 if self.svn_branch:
541 break
542 branch_spec = RunGit(
543 ['config', 'svn-remote.%s.branches' % remote],
544 error_ok=True).strip()
545 if branch_spec:
546 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
547 if self.svn_branch:
548 break
549 tag_spec = RunGit(
550 ['config', 'svn-remote.%s.tags' % remote],
551 error_ok=True).strip()
552 if tag_spec:
553 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
554 if self.svn_branch:
555 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556
557 if not self.svn_branch:
558 DieWithError('Can\'t guess svn branch -- try specifying it on the '
559 'command line')
560
561 return self.svn_branch
562
563 def GetTreeStatusUrl(self, error_ok=False):
564 if not self.tree_status_url:
565 error_message = ('You must configure your tree status URL by running '
566 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000567 self.tree_status_url = self._GetRietveldConfig(
568 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 return self.tree_status_url
570
571 def GetViewVCUrl(self):
572 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000573 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 return self.viewvc_url
575
rmistry@google.com90752582014-01-14 21:04:50 +0000576 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000577 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000578
rmistry@google.com78948ed2015-07-08 23:09:57 +0000579 def GetIsSkipDependencyUpload(self, branch_name):
580 """Returns true if specified branch should skip dep uploads."""
581 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
582 error_ok=True)
583
rmistry@google.com5626a922015-02-26 14:03:30 +0000584 def GetRunPostUploadHook(self):
585 run_post_upload_hook = self._GetRietveldConfig(
586 'run-post-upload-hook', error_ok=True)
587 return run_post_upload_hook == "True"
588
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000589 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000590 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000591
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000592 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000593 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000594
ukai@chromium.orge8077812012-02-03 03:41:46 +0000595 def GetIsGerrit(self):
596 """Return true if this repo is assosiated with gerrit code review system."""
597 if self.is_gerrit is None:
598 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
599 return self.is_gerrit
600
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000601 def GetGitEditor(self):
602 """Return the editor specified in the git config, or None if none is."""
603 if self.git_editor is None:
604 self.git_editor = self._GetConfig('core.editor', error_ok=True)
605 return self.git_editor or None
606
thestig@chromium.org44202a22014-03-11 19:22:18 +0000607 def GetLintRegex(self):
608 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
609 DEFAULT_LINT_REGEX)
610
611 def GetLintIgnoreRegex(self):
612 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
613 DEFAULT_LINT_IGNORE_REGEX)
614
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000615 def GetProject(self):
616 if not self.project:
617 self.project = self._GetRietveldConfig('project', error_ok=True)
618 return self.project
619
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000620 def GetForceHttpsCommitUrl(self):
621 if not self.force_https_commit_url:
622 self.force_https_commit_url = self._GetRietveldConfig(
623 'force-https-commit-url', error_ok=True)
624 return self.force_https_commit_url
625
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000626 def GetPendingRefPrefix(self):
627 if not self.pending_ref_prefix:
628 self.pending_ref_prefix = self._GetRietveldConfig(
629 'pending-ref-prefix', error_ok=True)
630 return self.pending_ref_prefix
631
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632 def _GetRietveldConfig(self, param, **kwargs):
633 return self._GetConfig('rietveld.' + param, **kwargs)
634
rmistry@google.com78948ed2015-07-08 23:09:57 +0000635 def _GetBranchConfig(self, branch_name, param, **kwargs):
636 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 def _GetConfig(self, param, **kwargs):
639 self.LazyUpdateIfNeeded()
640 return RunGit(['config', param], **kwargs).strip()
641
642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643def ShortBranchName(branch):
644 """Convert a name like 'refs/heads/foo' to just 'foo'."""
645 return branch.replace('refs/heads/', '')
646
647
648class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000649 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000651 global settings
652 if not settings:
653 # Happens when git_cl.py is used as a utility library.
654 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 settings.GetDefaultServerUrl()
656 self.branchref = branchref
657 if self.branchref:
658 self.branch = ShortBranchName(self.branchref)
659 else:
660 self.branch = None
661 self.rietveld_server = None
662 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000663 self.lookedup_issue = False
664 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 self.has_description = False
666 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000667 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000669 self.cc = None
670 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000671 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000673 self._remote = None
674 self._rpc_server = None
675
676 @property
677 def auth_config(self):
678 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000679
680 def GetCCList(self):
681 """Return the users cc'd on this CL.
682
683 Return is a string suitable for passing to gcl with the --cc flag.
684 """
685 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000686 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000687 more_cc = ','.join(self.watchers)
688 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
689 return self.cc
690
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000691 def GetCCListWithoutDefault(self):
692 """Return the users cc'd on this CL excluding default ones."""
693 if self.cc is None:
694 self.cc = ','.join(self.watchers)
695 return self.cc
696
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000697 def SetWatchers(self, watchers):
698 """Set the list of email addresses that should be cc'd based on the changed
699 files in this CL.
700 """
701 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702
703 def GetBranch(self):
704 """Returns the short branch name, e.g. 'master'."""
705 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000706 branchref = RunGit(['symbolic-ref', 'HEAD'],
707 stderr=subprocess2.VOID, error_ok=True).strip()
708 if not branchref:
709 return None
710 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 self.branch = ShortBranchName(self.branchref)
712 return self.branch
713
714 def GetBranchRef(self):
715 """Returns the full branch name, e.g. 'refs/heads/master'."""
716 self.GetBranch() # Poke the lazy loader.
717 return self.branchref
718
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000719 @staticmethod
720 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000721 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 e.g. 'origin', 'refs/heads/master'
723 """
724 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
726 error_ok=True).strip()
727 if upstream_branch:
728 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
729 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000730 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
731 error_ok=True).strip()
732 if upstream_branch:
733 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000735 # Fall back on trying a git-svn upstream branch.
736 if settings.GetIsGitSvn():
737 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 # Else, try to guess the origin remote.
740 remote_branches = RunGit(['branch', '-r']).split()
741 if 'origin/master' in remote_branches:
742 # Fall back on origin/master if it exits.
743 remote = 'origin'
744 upstream_branch = 'refs/heads/master'
745 elif 'origin/trunk' in remote_branches:
746 # Fall back on origin/trunk if it exists. Generally a shared
747 # git-svn clone
748 remote = 'origin'
749 upstream_branch = 'refs/heads/trunk'
750 else:
751 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752Either pass complete "git diff"-style arguments, like
753 git cl upload origin/master
754or verify this branch is set up to track another (via the --track argument to
755"git checkout -b ...").""")
756
757 return remote, upstream_branch
758
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000760 upstream_branch = self.GetUpstreamBranch()
761 if not BranchExists(upstream_branch):
762 DieWithError('The upstream for the current branch (%s) does not exist '
763 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000764 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000765 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 def GetUpstreamBranch(self):
768 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000769 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000771 upstream_branch = upstream_branch.replace('refs/heads/',
772 'refs/remotes/%s/' % remote)
773 upstream_branch = upstream_branch.replace('refs/branch-heads/',
774 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 self.upstream_branch = upstream_branch
776 return self.upstream_branch
777
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000778 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000779 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 remote, branch = None, self.GetBranch()
781 seen_branches = set()
782 while branch not in seen_branches:
783 seen_branches.add(branch)
784 remote, branch = self.FetchUpstreamTuple(branch)
785 branch = ShortBranchName(branch)
786 if remote != '.' or branch.startswith('refs/remotes'):
787 break
788 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000789 remotes = RunGit(['remote'], error_ok=True).split()
790 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000791 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000792 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000793 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000794 logging.warning('Could not determine which remote this change is '
795 'associated with, so defaulting to "%s". This may '
796 'not be what you want. You may prevent this message '
797 'by running "git svn info" as documented here: %s',
798 self._remote,
799 GIT_INSTRUCTIONS_URL)
800 else:
801 logging.warn('Could not determine which remote this change is '
802 'associated with. You may prevent this message by '
803 'running "git svn info" as documented here: %s',
804 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000805 branch = 'HEAD'
806 if branch.startswith('refs/remotes'):
807 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000808 elif branch.startswith('refs/branch-heads/'):
809 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000810 else:
811 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000812 return self._remote
813
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000814 def GitSanityChecks(self, upstream_git_obj):
815 """Checks git repo status and ensures diff is from local commits."""
816
sbc@chromium.org79706062015-01-14 21:18:12 +0000817 if upstream_git_obj is None:
818 if self.GetBranch() is None:
819 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000820 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000821 else:
822 print >> sys.stderr, (
823 'ERROR: no upstream branch')
824 return False
825
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000826 # Verify the commit we're diffing against is in our current branch.
827 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
828 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
829 if upstream_sha != common_ancestor:
830 print >> sys.stderr, (
831 'ERROR: %s is not in the current branch. You may need to rebase '
832 'your tracking branch' % upstream_sha)
833 return False
834
835 # List the commits inside the diff, and verify they are all local.
836 commits_in_diff = RunGit(
837 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
838 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
839 remote_branch = remote_branch.strip()
840 if code != 0:
841 _, remote_branch = self.GetRemoteBranch()
842
843 commits_in_remote = RunGit(
844 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
845
846 common_commits = set(commits_in_diff) & set(commits_in_remote)
847 if common_commits:
848 print >> sys.stderr, (
849 'ERROR: Your diff contains %d commits already in %s.\n'
850 'Run "git log --oneline %s..HEAD" to get a list of commits in '
851 'the diff. If you are using a custom git flow, you can override'
852 ' the reference used for this check with "git config '
853 'gitcl.remotebranch <git-ref>".' % (
854 len(common_commits), remote_branch, upstream_git_obj))
855 return False
856 return True
857
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000858 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000859 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000860
861 Returns None if it is not set.
862 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000863 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
864 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000865
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000866 def GetGitSvnRemoteUrl(self):
867 """Return the configured git-svn remote URL parsed from git svn info.
868
869 Returns None if it is not set.
870 """
871 # URL is dependent on the current directory.
872 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
873 if data:
874 keys = dict(line.split(': ', 1) for line in data.splitlines()
875 if ': ' in line)
876 return keys.get('URL', None)
877 return None
878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 def GetRemoteUrl(self):
880 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
881
882 Returns None if there is no remote.
883 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000884 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000885 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
886
887 # If URL is pointing to a local directory, it is probably a git cache.
888 if os.path.isdir(url):
889 url = RunGit(['config', 'remote.%s.url' % remote],
890 error_ok=True,
891 cwd=url).strip()
892 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893
894 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000895 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000896 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000898 self.issue = int(issue) or None if issue else None
899 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 return self.issue
901
902 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000903 if not self.rietveld_server:
904 # If we're on a branch then get the server potentially associated
905 # with that branch.
906 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000907 rietveld_server_config = self._RietveldServer()
908 if rietveld_server_config:
909 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
910 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000911 if not self.rietveld_server:
912 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 return self.rietveld_server
914
915 def GetIssueURL(self):
916 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000917 if not self.GetIssue():
918 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
920
921 def GetDescription(self, pretty=False):
922 if not self.has_description:
923 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000924 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000925 try:
926 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000927 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000928 if e.code == 404:
929 DieWithError(
930 ('\nWhile fetching the description for issue %d, received a '
931 '404 (not found)\n'
932 'error. It is likely that you deleted this '
933 'issue on the server. If this is the\n'
934 'case, please run\n\n'
935 ' git cl issue 0\n\n'
936 'to clear the association with the deleted issue. Then run '
937 'this command again.') % issue)
938 else:
939 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000940 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000941 except urllib2.URLError as e:
942 print >> sys.stderr, (
943 'Warning: Failed to retrieve CL description due to network '
944 'failure.')
945 self.description = ''
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 self.has_description = True
948 if pretty:
949 wrapper = textwrap.TextWrapper()
950 wrapper.initial_indent = wrapper.subsequent_indent = ' '
951 return wrapper.fill(self.description)
952 return self.description
953
954 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000955 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 patchset = RunGit(['config', self._PatchsetSetting()],
958 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 self.patchset = int(patchset) or None if patchset else None
960 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 return self.patchset
962
963 def SetPatchset(self, patchset):
964 """Set this branch's patchset. If patchset=0, clears the patchset."""
965 if patchset:
966 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000967 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 else:
969 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000970 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000971 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000973 def GetMostRecentPatchset(self):
974 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000975
976 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000977 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000978 '/download/issue%s_%s.diff' % (issue, patchset))
979
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000980 def GetIssueProperties(self):
981 if self._props is None:
982 issue = self.GetIssue()
983 if not issue:
984 self._props = {}
985 else:
986 self._props = self.RpcServer().get_issue_properties(issue, True)
987 return self._props
988
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000989 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000990 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000991
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000992 def AddComment(self, message):
993 return self.RpcServer().add_comment(self.GetIssue(), message)
994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 def SetIssue(self, issue):
996 """Set this branch's issue. If issue=0, clears the issue."""
997 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000998 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 RunGit(['config', self._IssueSetting(), str(issue)])
1000 if self.rietveld_server:
1001 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1002 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001003 current_issue = self.GetIssue()
1004 if current_issue:
1005 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001006 self.issue = None
1007 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001009 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001010 if not self.GitSanityChecks(upstream_branch):
1011 DieWithError('\nGit sanity check failure')
1012
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001013 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001014 if not root:
1015 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001016 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001017
1018 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001019 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001020 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001021 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001022 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001023 except subprocess2.CalledProcessError:
1024 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001025 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001026 'This branch probably doesn\'t exist anymore. To reset the\n'
1027 'tracking branch, please run\n'
1028 ' git branch --set-upstream %s trunk\n'
1029 'replacing trunk with origin/master or the relevant branch') %
1030 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001031
maruel@chromium.org52424302012-08-29 15:14:30 +00001032 issue = self.GetIssue()
1033 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001034 if issue:
1035 description = self.GetDescription()
1036 else:
1037 # If the change was never uploaded, use the log messages of all commits
1038 # up to the branch point, as git cl upload will prefill the description
1039 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1041 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001042
1043 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001044 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001045 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001046 name,
1047 description,
1048 absroot,
1049 files,
1050 issue,
1051 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001052 author,
1053 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001054
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001055 def GetStatus(self):
1056 """Apply a rough heuristic to give a simple summary of an issue's review
1057 or CQ status, assuming adherence to a common workflow.
1058
1059 Returns None if no issue for this branch, or one of the following keywords:
1060 * 'error' - error from review tool (including deleted issues)
1061 * 'unsent' - not sent for review
1062 * 'waiting' - waiting for review
1063 * 'reply' - waiting for owner to reply to review
1064 * 'lgtm' - LGTM from at least one approved reviewer
1065 * 'commit' - in the commit queue
1066 * 'closed' - closed
1067 """
1068 if not self.GetIssue():
1069 return None
1070
1071 try:
1072 props = self.GetIssueProperties()
1073 except urllib2.HTTPError:
1074 return 'error'
1075
1076 if props.get('closed'):
1077 # Issue is closed.
1078 return 'closed'
1079 if props.get('commit'):
1080 # Issue is in the commit queue.
1081 return 'commit'
1082
1083 try:
1084 reviewers = self.GetApprovingReviewers()
1085 except urllib2.HTTPError:
1086 return 'error'
1087
1088 if reviewers:
1089 # Was LGTM'ed.
1090 return 'lgtm'
1091
1092 messages = props.get('messages') or []
1093
1094 if not messages:
1095 # No message was sent.
1096 return 'unsent'
1097 if messages[-1]['sender'] != props.get('owner_email'):
1098 # Non-LGTM reply from non-owner
1099 return 'reply'
1100 return 'waiting'
1101
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001102 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001103 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001104
1105 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001106 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001107 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001108 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001109 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001110 except presubmit_support.PresubmitFailure, e:
1111 DieWithError(
1112 ('%s\nMaybe your depot_tools is out of date?\n'
1113 'If all fails, contact maruel@') % e)
1114
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001115 def UpdateDescription(self, description):
1116 self.description = description
1117 return self.RpcServer().update_description(
1118 self.GetIssue(), self.description)
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001121 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001122 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001124 def SetFlag(self, flag, value):
1125 """Patchset must match."""
1126 if not self.GetPatchset():
1127 DieWithError('The patchset needs to match. Send another patchset.')
1128 try:
1129 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001130 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001131 except urllib2.HTTPError, e:
1132 if e.code == 404:
1133 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1134 if e.code == 403:
1135 DieWithError(
1136 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1137 'match?') % (self.GetIssue(), self.GetPatchset()))
1138 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001140 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 """Returns an upload.RpcServer() to access this review's rietveld instance.
1142 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001143 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001144 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001145 self.GetRietveldServer(),
1146 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001147 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
1149 def _IssueSetting(self):
1150 """Return the git setting that stores this change's issue."""
1151 return 'branch.%s.rietveldissue' % self.GetBranch()
1152
1153 def _PatchsetSetting(self):
1154 """Return the git setting that stores this change's most recent patchset."""
1155 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1156
1157 def _RietveldServer(self):
1158 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001159 branch = self.GetBranch()
1160 if branch:
1161 return 'branch.%s.rietveldserver' % branch
1162 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164
1165def GetCodereviewSettingsInteractively():
1166 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001167 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 server = settings.GetDefaultServerUrl(error_ok=True)
1169 prompt = 'Rietveld server (host[:port])'
1170 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001171 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 if not server and not newserver:
1173 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001174 if newserver:
1175 newserver = gclient_utils.UpgradeToHttps(newserver)
1176 if newserver != server:
1177 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001179 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 prompt = caption
1181 if initial:
1182 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001183 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 if new_val == 'x':
1185 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001186 elif new_val:
1187 if is_url:
1188 new_val = gclient_utils.UpgradeToHttps(new_val)
1189 if new_val != initial:
1190 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001192 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001193 SetProperty(settings.GetDefaultPrivateFlag(),
1194 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001196 'tree-status-url', False)
1197 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001198 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001199 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1200 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
1202 # TODO: configure a default branch to diff against, rather than this
1203 # svn-based hackery.
1204
1205
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001206class ChangeDescription(object):
1207 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001208 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001209 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001210
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001212 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001213
agable@chromium.org42c20792013-09-12 17:34:49 +00001214 @property # www.logilab.org/ticket/89786
1215 def description(self): # pylint: disable=E0202
1216 return '\n'.join(self._description_lines)
1217
1218 def set_description(self, desc):
1219 if isinstance(desc, basestring):
1220 lines = desc.splitlines()
1221 else:
1222 lines = [line.rstrip() for line in desc]
1223 while lines and not lines[0]:
1224 lines.pop(0)
1225 while lines and not lines[-1]:
1226 lines.pop(-1)
1227 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001228
piman@chromium.org336f9122014-09-04 02:16:55 +00001229 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001230 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001231 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001232 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001233 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001234 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001235
agable@chromium.org42c20792013-09-12 17:34:49 +00001236 # Get the set of R= and TBR= lines and remove them from the desciption.
1237 regexp = re.compile(self.R_LINE)
1238 matches = [regexp.match(line) for line in self._description_lines]
1239 new_desc = [l for i, l in enumerate(self._description_lines)
1240 if not matches[i]]
1241 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001242
agable@chromium.org42c20792013-09-12 17:34:49 +00001243 # Construct new unified R= and TBR= lines.
1244 r_names = []
1245 tbr_names = []
1246 for match in matches:
1247 if not match:
1248 continue
1249 people = cleanup_list([match.group(2).strip()])
1250 if match.group(1) == 'TBR':
1251 tbr_names.extend(people)
1252 else:
1253 r_names.extend(people)
1254 for name in r_names:
1255 if name not in reviewers:
1256 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001257 if add_owners_tbr:
1258 owners_db = owners.Database(change.RepositoryRoot(),
1259 fopen=file, os_path=os.path, glob=glob.glob)
1260 all_reviewers = set(tbr_names + reviewers)
1261 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1262 all_reviewers)
1263 tbr_names.extend(owners_db.reviewers_for(missing_files,
1264 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001265 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1266 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1267
1268 # Put the new lines in the description where the old first R= line was.
1269 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1270 if 0 <= line_loc < len(self._description_lines):
1271 if new_tbr_line:
1272 self._description_lines.insert(line_loc, new_tbr_line)
1273 if new_r_line:
1274 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001275 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 if new_r_line:
1277 self.append_footer(new_r_line)
1278 if new_tbr_line:
1279 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001280
1281 def prompt(self):
1282 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001283 self.set_description([
1284 '# Enter a description of the change.',
1285 '# This will be displayed on the codereview site.',
1286 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001287 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001288 '--------------------',
1289 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290
agable@chromium.org42c20792013-09-12 17:34:49 +00001291 regexp = re.compile(self.BUG_LINE)
1292 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001293 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001294 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001295 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001296 if not content:
1297 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001298 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001299
1300 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001301 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1302 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001303 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001304 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001305
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001306 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001307 if self._description_lines:
1308 # Add an empty line if either the last line or the new line isn't a tag.
1309 last_line = self._description_lines[-1]
1310 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1311 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1312 self._description_lines.append('')
1313 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001314
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001315 def get_reviewers(self):
1316 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001317 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1318 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001319 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001320
1321
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001322def get_approving_reviewers(props):
1323 """Retrieves the reviewers that approved a CL from the issue properties with
1324 messages.
1325
1326 Note that the list may contain reviewers that are not committer, thus are not
1327 considered by the CQ.
1328 """
1329 return sorted(
1330 set(
1331 message['sender']
1332 for message in props['messages']
1333 if message['approval'] and message['sender'] in props['reviewers']
1334 )
1335 )
1336
1337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338def FindCodereviewSettingsFile(filename='codereview.settings'):
1339 """Finds the given file starting in the cwd and going up.
1340
1341 Only looks up to the top of the repository unless an
1342 'inherit-review-settings-ok' file exists in the root of the repository.
1343 """
1344 inherit_ok_file = 'inherit-review-settings-ok'
1345 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001346 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1348 root = '/'
1349 while True:
1350 if filename in os.listdir(cwd):
1351 if os.path.isfile(os.path.join(cwd, filename)):
1352 return open(os.path.join(cwd, filename))
1353 if cwd == root:
1354 break
1355 cwd = os.path.dirname(cwd)
1356
1357
1358def LoadCodereviewSettingsFromFile(fileobj):
1359 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001360 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def SetProperty(name, setting, unset_error_ok=False):
1363 fullname = 'rietveld.' + name
1364 if setting in keyvals:
1365 RunGit(['config', fullname, keyvals[setting]])
1366 else:
1367 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1368
1369 SetProperty('server', 'CODE_REVIEW_SERVER')
1370 # Only server setting is required. Other settings can be absent.
1371 # In that case, we ignore errors raised during option deletion attempt.
1372 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001373 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1375 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001376 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001377 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001378 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1379 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001380 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001381 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001382 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001383 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1384 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001386 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1390 #should be of the form
1391 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1392 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1393 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1394 keyvals['ORIGIN_URL_CONFIG']])
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001397def urlretrieve(source, destination):
1398 """urllib is broken for SSL connections via a proxy therefore we
1399 can't use urllib.urlretrieve()."""
1400 with open(destination, 'w') as f:
1401 f.write(urllib2.urlopen(source).read())
1402
1403
ukai@chromium.org712d6102013-11-27 00:52:58 +00001404def hasSheBang(fname):
1405 """Checks fname is a #! script."""
1406 with open(fname) as f:
1407 return f.read(2).startswith('#!')
1408
1409
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001410def DownloadHooks(force):
1411 """downloads hooks
1412
1413 Args:
1414 force: True to update hooks. False to install hooks if not present.
1415 """
1416 if not settings.GetIsGerrit():
1417 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001418 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001419 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1420 if not os.access(dst, os.X_OK):
1421 if os.path.exists(dst):
1422 if not force:
1423 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001424 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001425 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001426 if not hasSheBang(dst):
1427 DieWithError('Not a script: %s\n'
1428 'You need to download from\n%s\n'
1429 'into .git/hooks/commit-msg and '
1430 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001431 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1432 except Exception:
1433 if os.path.exists(dst):
1434 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001435 DieWithError('\nFailed to download hooks.\n'
1436 'You need to download from\n%s\n'
1437 'into .git/hooks/commit-msg and '
1438 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001439
1440
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001441@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001443 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001445 parser.add_option('--activate-update', action='store_true',
1446 help='activate auto-updating [rietveld] section in '
1447 '.git/config')
1448 parser.add_option('--deactivate-update', action='store_true',
1449 help='deactivate auto-updating [rietveld] section in '
1450 '.git/config')
1451 options, args = parser.parse_args(args)
1452
1453 if options.deactivate_update:
1454 RunGit(['config', 'rietveld.autoupdate', 'false'])
1455 return
1456
1457 if options.activate_update:
1458 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1459 return
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if len(args) == 0:
1462 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001463 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 return 0
1465
1466 url = args[0]
1467 if not url.endswith('codereview.settings'):
1468 url = os.path.join(url, 'codereview.settings')
1469
1470 # Load code review settings and download hooks (if available).
1471 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001472 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 return 0
1474
1475
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001476def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001477 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001478 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1479 branch = ShortBranchName(branchref)
1480 _, args = parser.parse_args(args)
1481 if not args:
1482 print("Current base-url:")
1483 return RunGit(['config', 'branch.%s.base-url' % branch],
1484 error_ok=False).strip()
1485 else:
1486 print("Setting base-url to %s" % args[0])
1487 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1488 error_ok=False).strip()
1489
1490
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001491def color_for_status(status):
1492 """Maps a Changelist status to color, for CMDstatus and other tools."""
1493 return {
1494 'unsent': Fore.RED,
1495 'waiting': Fore.BLUE,
1496 'reply': Fore.YELLOW,
1497 'lgtm': Fore.GREEN,
1498 'commit': Fore.MAGENTA,
1499 'closed': Fore.CYAN,
1500 'error': Fore.WHITE,
1501 }.get(status, Fore.WHITE)
1502
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001503def fetch_cl_status(branch, auth_config=None):
1504 """Fetches information for an issue and returns (branch, issue, status)."""
1505 cl = Changelist(branchref=branch, auth_config=auth_config)
1506 url = cl.GetIssueURL()
1507 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001508
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001509 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001510 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001511 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001512
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001513 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001514
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001515def get_cl_statuses(
1516 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001517 """Returns a blocking iterable of (branch, issue, color) for given branches.
1518
1519 If fine_grained is true, this will fetch CL statuses from the server.
1520 Otherwise, simply indicate if there's a matching url for the given branches.
1521
1522 If max_processes is specified, it is used as the maximum number of processes
1523 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1524 spawned.
1525 """
1526 # Silence upload.py otherwise it becomes unwieldly.
1527 upload.verbosity = 0
1528
1529 if fine_grained:
1530 # Process one branch synchronously to work through authentication, then
1531 # spawn processes to process all the other branches in parallel.
1532 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001533 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1534 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001535
1536 branches_to_fetch = branches[1:]
1537 pool = ThreadPool(
1538 min(max_processes, len(branches_to_fetch))
1539 if max_processes is not None
1540 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001541 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001542 yield x
1543 else:
1544 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1545 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001546 cl = Changelist(branchref=b, auth_config=auth_config)
1547 url = cl.GetIssueURL()
1548 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001549
rmistry@google.com2dd99862015-06-22 12:22:18 +00001550
1551def upload_branch_deps(cl, args):
1552 """Uploads CLs of local branches that are dependents of the current branch.
1553
1554 If the local branch dependency tree looks like:
1555 test1 -> test2.1 -> test3.1
1556 -> test3.2
1557 -> test2.2 -> test3.3
1558
1559 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1560 run on the dependent branches in this order:
1561 test2.1, test3.1, test3.2, test2.2, test3.3
1562
1563 Note: This function does not rebase your local dependent branches. Use it when
1564 you make a change to the parent branch that will not conflict with its
1565 dependent branches, and you would like their dependencies updated in
1566 Rietveld.
1567 """
1568 if git_common.is_dirty_git_tree('upload-branch-deps'):
1569 return 1
1570
1571 root_branch = cl.GetBranch()
1572 if root_branch is None:
1573 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1574 'Get on a branch!')
1575 if not cl.GetIssue() or not cl.GetPatchset():
1576 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1577 'patchset dependencies without an uploaded CL.')
1578
1579 branches = RunGit(['for-each-ref',
1580 '--format=%(refname:short) %(upstream:short)',
1581 'refs/heads'])
1582 if not branches:
1583 print('No local branches found.')
1584 return 0
1585
1586 # Create a dictionary of all local branches to the branches that are dependent
1587 # on it.
1588 tracked_to_dependents = collections.defaultdict(list)
1589 for b in branches.splitlines():
1590 tokens = b.split()
1591 if len(tokens) == 2:
1592 branch_name, tracked = tokens
1593 tracked_to_dependents[tracked].append(branch_name)
1594
1595 print
1596 print 'The dependent local branches of %s are:' % root_branch
1597 dependents = []
1598 def traverse_dependents_preorder(branch, padding=''):
1599 dependents_to_process = tracked_to_dependents.get(branch, [])
1600 padding += ' '
1601 for dependent in dependents_to_process:
1602 print '%s%s' % (padding, dependent)
1603 dependents.append(dependent)
1604 traverse_dependents_preorder(dependent, padding)
1605 traverse_dependents_preorder(root_branch)
1606 print
1607
1608 if not dependents:
1609 print 'There are no dependent local branches for %s' % root_branch
1610 return 0
1611
1612 print ('This command will checkout all dependent branches and run '
1613 '"git cl upload".')
1614 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1615
1616 # Add a default patchset title to all upload calls.
1617 args.extend(['-t', 'Updated patchset dependency'])
1618 # Record all dependents that failed to upload.
1619 failures = {}
1620 # Go through all dependents, checkout the branch and upload.
1621 try:
1622 for dependent_branch in dependents:
1623 print
1624 print '--------------------------------------'
1625 print 'Running "git cl upload" from %s:' % dependent_branch
1626 RunGit(['checkout', '-q', dependent_branch])
1627 print
1628 try:
1629 if CMDupload(OptionParser(), args) != 0:
1630 print 'Upload failed for %s!' % dependent_branch
1631 failures[dependent_branch] = 1
1632 except: # pylint: disable=W0702
1633 failures[dependent_branch] = 1
1634 print
1635 finally:
1636 # Swap back to the original root branch.
1637 RunGit(['checkout', '-q', root_branch])
1638
1639 print
1640 print 'Upload complete for dependent branches!'
1641 for dependent_branch in dependents:
1642 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1643 print ' %s : %s' % (dependent_branch, upload_status)
1644 print
1645
1646 return 0
1647
1648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001650 """Show status of changelists.
1651
1652 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001653 - Red not sent for review or broken
1654 - Blue waiting for review
1655 - Yellow waiting for you to reply to review
1656 - Green LGTM'ed
1657 - Magenta in the commit queue
1658 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001659
1660 Also see 'git cl comments'.
1661 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 parser.add_option('--field',
1663 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001664 parser.add_option('-f', '--fast', action='store_true',
1665 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001666 parser.add_option(
1667 '-j', '--maxjobs', action='store', type=int,
1668 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001669
1670 auth.add_auth_options(parser)
1671 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001672 if args:
1673 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001676 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001677 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 if options.field.startswith('desc'):
1679 print cl.GetDescription()
1680 elif options.field == 'id':
1681 issueid = cl.GetIssue()
1682 if issueid:
1683 print issueid
1684 elif options.field == 'patch':
1685 patchset = cl.GetPatchset()
1686 if patchset:
1687 print patchset
1688 elif options.field == 'url':
1689 url = cl.GetIssueURL()
1690 if url:
1691 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001692 return 0
1693
1694 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1695 if not branches:
1696 print('No local branch found.')
1697 return 0
1698
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001699 changes = (
1700 Changelist(branchref=b, auth_config=auth_config)
1701 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001702 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001703 alignment = max(5, max(len(b) for b in branches))
1704 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001705 output = get_cl_statuses(branches,
1706 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001707 max_processes=options.maxjobs,
1708 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001709
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001710 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001711 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001712 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001713 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001714 b, i, status = output.next()
1715 branch_statuses[b] = (i, status)
1716 issue_url, status = branch_statuses.pop(branch)
1717 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001718 reset = Fore.RESET
1719 if not sys.stdout.isatty():
1720 color = ''
1721 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001722 status_str = '(%s)' % status if status else ''
1723 print ' %*s : %s%s %s%s' % (
1724 alignment, ShortBranchName(branch), color, issue_url, status_str,
1725 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001726
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001727 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001728 print
1729 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001730 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001731 if not cl.GetIssue():
1732 print 'No issue assigned.'
1733 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001734 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001735 if not options.fast:
1736 print 'Issue description:'
1737 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 return 0
1739
1740
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001741def colorize_CMDstatus_doc():
1742 """To be called once in main() to add colors to git cl status help."""
1743 colors = [i for i in dir(Fore) if i[0].isupper()]
1744
1745 def colorize_line(line):
1746 for color in colors:
1747 if color in line.upper():
1748 # Extract whitespaces first and the leading '-'.
1749 indent = len(line) - len(line.lstrip(' ')) + 1
1750 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1751 return line
1752
1753 lines = CMDstatus.__doc__.splitlines()
1754 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1755
1756
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001757@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001758def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001759 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
1761 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001762 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001763 parser.add_option('-r', '--reverse', action='store_true',
1764 help='Lookup the branch(es) for the specified issues. If '
1765 'no issues are specified, all branches with mapped '
1766 'issues will be listed.')
1767 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
dnj@chromium.org406c4402015-03-03 17:22:28 +00001769 if options.reverse:
1770 branches = RunGit(['for-each-ref', 'refs/heads',
1771 '--format=%(refname:short)']).splitlines()
1772
1773 # Reverse issue lookup.
1774 issue_branch_map = {}
1775 for branch in branches:
1776 cl = Changelist(branchref=branch)
1777 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1778 if not args:
1779 args = sorted(issue_branch_map.iterkeys())
1780 for issue in args:
1781 if not issue:
1782 continue
1783 print 'Branch for issue number %s: %s' % (
1784 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1785 else:
1786 cl = Changelist()
1787 if len(args) > 0:
1788 try:
1789 issue = int(args[0])
1790 except ValueError:
1791 DieWithError('Pass a number to set the issue or none to list it.\n'
1792 'Maybe you want to run git cl status?')
1793 cl.SetIssue(issue)
1794 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 return 0
1796
1797
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001798def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001799 """Shows or posts review comments for any changelist."""
1800 parser.add_option('-a', '--add-comment', dest='comment',
1801 help='comment to add to an issue')
1802 parser.add_option('-i', dest='issue',
1803 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001804 parser.add_option('-j', '--json-file',
1805 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001806 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001807 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001808 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001809
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001810 issue = None
1811 if options.issue:
1812 try:
1813 issue = int(options.issue)
1814 except ValueError:
1815 DieWithError('A review issue id is expected to be a number')
1816
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001817 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001818
1819 if options.comment:
1820 cl.AddComment(options.comment)
1821 return 0
1822
1823 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001824 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001825 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001826 summary.append({
1827 'date': message['date'],
1828 'lgtm': False,
1829 'message': message['text'],
1830 'not_lgtm': False,
1831 'sender': message['sender'],
1832 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001833 if message['disapproval']:
1834 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001835 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001836 elif message['approval']:
1837 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001838 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001839 elif message['sender'] == data['owner_email']:
1840 color = Fore.MAGENTA
1841 else:
1842 color = Fore.BLUE
1843 print '\n%s%s %s%s' % (
1844 color, message['date'].split('.', 1)[0], message['sender'],
1845 Fore.RESET)
1846 if message['text'].strip():
1847 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001848 if options.json_file:
1849 with open(options.json_file, 'wb') as f:
1850 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001851 return 0
1852
1853
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001854def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001855 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001856 parser.add_option('-d', '--display', action='store_true',
1857 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001858 auth.add_auth_options(parser)
1859 options, _ = parser.parse_args(args)
1860 auth_config = auth.extract_auth_config_from_options(options)
1861 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001862 if not cl.GetIssue():
1863 DieWithError('This branch has no associated changelist.')
1864 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001865 if options.display:
1866 print description.description
1867 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001868 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001869 if cl.GetDescription() != description.description:
1870 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001871 return 0
1872
1873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874def CreateDescriptionFromLog(args):
1875 """Pulls out the commit log to use as a base for the CL description."""
1876 log_args = []
1877 if len(args) == 1 and not args[0].endswith('.'):
1878 log_args = [args[0] + '..']
1879 elif len(args) == 1 and args[0].endswith('...'):
1880 log_args = [args[0][:-1]]
1881 elif len(args) == 2:
1882 log_args = [args[0] + '..' + args[1]]
1883 else:
1884 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001885 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886
1887
thestig@chromium.org44202a22014-03-11 19:22:18 +00001888def CMDlint(parser, args):
1889 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001890 parser.add_option('--filter', action='append', metavar='-x,+y',
1891 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001892 auth.add_auth_options(parser)
1893 options, args = parser.parse_args(args)
1894 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001895
1896 # Access to a protected member _XX of a client class
1897 # pylint: disable=W0212
1898 try:
1899 import cpplint
1900 import cpplint_chromium
1901 except ImportError:
1902 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1903 return 1
1904
1905 # Change the current working directory before calling lint so that it
1906 # shows the correct base.
1907 previous_cwd = os.getcwd()
1908 os.chdir(settings.GetRoot())
1909 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001910 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001911 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1912 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001913 if not files:
1914 print "Cannot lint an empty CL"
1915 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001916
1917 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001918 command = args + files
1919 if options.filter:
1920 command = ['--filter=' + ','.join(options.filter)] + command
1921 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001922
1923 white_regex = re.compile(settings.GetLintRegex())
1924 black_regex = re.compile(settings.GetLintIgnoreRegex())
1925 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1926 for filename in filenames:
1927 if white_regex.match(filename):
1928 if black_regex.match(filename):
1929 print "Ignoring file %s" % filename
1930 else:
1931 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1932 extra_check_functions)
1933 else:
1934 print "Skipping file %s" % filename
1935 finally:
1936 os.chdir(previous_cwd)
1937 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1938 if cpplint._cpplint_state.error_count != 0:
1939 return 1
1940 return 0
1941
1942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001944 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001945 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001947 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001948 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001949 auth.add_auth_options(parser)
1950 options, args = parser.parse_args(args)
1951 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952
sbc@chromium.org71437c02015-04-09 19:29:40 +00001953 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001954 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955 return 1
1956
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001957 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 if args:
1959 base_branch = args[0]
1960 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001961 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001962 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001964 cl.RunHook(
1965 committing=not options.upload,
1966 may_prompt=False,
1967 verbose=options.verbose,
1968 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001969 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970
1971
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001972def AddChangeIdToCommitMessage(options, args):
1973 """Re-commits using the current message, assumes the commit hook is in
1974 place.
1975 """
1976 log_desc = options.message or CreateDescriptionFromLog(args)
1977 git_command = ['commit', '--amend', '-m', log_desc]
1978 RunGit(git_command)
1979 new_log_desc = CreateDescriptionFromLog(args)
1980 if CHANGE_ID in new_log_desc:
1981 print 'git-cl: Added Change-Id to commit message.'
1982 else:
1983 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1984
1985
piman@chromium.org336f9122014-09-04 02:16:55 +00001986def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001987 """upload the current branch to gerrit."""
1988 # We assume the remote called "origin" is the one we want.
1989 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001990 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001991
1992 remote, remote_branch = cl.GetRemoteBranch()
1993 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1994 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001996 change_desc = ChangeDescription(
1997 options.message or CreateDescriptionFromLog(args))
1998 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001999 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002001
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002002 if options.squash:
2003 # Try to get the message from a previous upload.
2004 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
2005 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
2006 if not message:
2007 if not options.force:
2008 change_desc.prompt()
2009
2010 if CHANGE_ID not in change_desc.description:
2011 # Run the commit-msg hook without modifying the head commit by writing
2012 # the commit message to a temporary file and running the hook over it,
2013 # then reading the file back in.
2014 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2015 'commit-msg')
2016 file_handle, msg_file = tempfile.mkstemp(text=True,
2017 prefix='commit_msg')
2018 try:
2019 try:
2020 with os.fdopen(file_handle, 'w') as fileobj:
2021 fileobj.write(change_desc.description)
2022 finally:
2023 os.close(file_handle)
2024 RunCommand([commit_msg_hook, msg_file])
2025 change_desc.set_description(gclient_utils.FileRead(msg_file))
2026 finally:
2027 os.remove(msg_file)
2028
2029 if not change_desc.description:
2030 print "Description is empty; aborting."
2031 return 1
2032
2033 message = change_desc.description
2034
2035 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2036 if remote is '.':
2037 # If our upstream branch is local, we base our squashed commit on its
2038 # squashed version.
2039 parent = ('refs/heads/git_cl_uploads/' +
2040 scm.GIT.ShortBranchName(upstream_branch))
2041
2042 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2043 # will create additional CLs when uploading.
2044 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2045 RunGitSilent(['rev-parse', parent + ':'])):
2046 print 'Upload upstream branch ' + upstream_branch + ' first.'
2047 return 1
2048 else:
2049 parent = cl.GetCommonAncestorWithUpstream()
2050
2051 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2052 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2053 '-m', message]).strip()
2054 else:
2055 if CHANGE_ID not in change_desc.description:
2056 AddChangeIdToCommitMessage(options, args)
2057 ref_to_push = 'HEAD'
2058 parent = '%s/%s' % (gerrit_remote, branch)
2059
2060 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2061 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002062 if len(commits) > 1:
2063 print('WARNING: This will upload %d commits. Run the following command '
2064 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002065 print('git log %s..%s' % (parent, ref_to_push))
2066 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002067 'commit.')
2068 ask_for_data('About to upload; enter to confirm.')
2069
piman@chromium.org336f9122014-09-04 02:16:55 +00002070 if options.reviewers or options.tbr_owners:
2071 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072
ukai@chromium.orge8077812012-02-03 03:41:46 +00002073 receive_options = []
2074 cc = cl.GetCCList().split(',')
2075 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002076 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002077 cc = filter(None, cc)
2078 if cc:
2079 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002080 if change_desc.get_reviewers():
2081 receive_options.extend(
2082 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083
ukai@chromium.orge8077812012-02-03 03:41:46 +00002084 git_command = ['push']
2085 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002086 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002087 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002088 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002089 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002090
2091 if options.squash:
2092 head = RunGit(['rev-parse', 'HEAD']).strip()
2093 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2094
ukai@chromium.orge8077812012-02-03 03:41:46 +00002095 # TODO(ukai): parse Change-Id: and set issue number?
2096 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
wittman@chromium.org455dc922015-01-26 20:15:50 +00002099def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2100 """Computes the remote branch ref to use for the CL.
2101
2102 Args:
2103 remote (str): The git remote for the CL.
2104 remote_branch (str): The git remote branch for the CL.
2105 target_branch (str): The target branch specified by the user.
2106 pending_prefix (str): The pending prefix from the settings.
2107 """
2108 if not (remote and remote_branch):
2109 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002110
wittman@chromium.org455dc922015-01-26 20:15:50 +00002111 if target_branch:
2112 # Cannonicalize branch references to the equivalent local full symbolic
2113 # refs, which are then translated into the remote full symbolic refs
2114 # below.
2115 if '/' not in target_branch:
2116 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2117 else:
2118 prefix_replacements = (
2119 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2120 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2121 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2122 )
2123 match = None
2124 for regex, replacement in prefix_replacements:
2125 match = re.search(regex, target_branch)
2126 if match:
2127 remote_branch = target_branch.replace(match.group(0), replacement)
2128 break
2129 if not match:
2130 # This is a branch path but not one we recognize; use as-is.
2131 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002132 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2133 # Handle the refs that need to land in different refs.
2134 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002135
wittman@chromium.org455dc922015-01-26 20:15:50 +00002136 # Create the true path to the remote branch.
2137 # Does the following translation:
2138 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2139 # * refs/remotes/origin/master -> refs/heads/master
2140 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2141 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2142 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2143 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2144 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2145 'refs/heads/')
2146 elif remote_branch.startswith('refs/remotes/branch-heads'):
2147 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2148 # If a pending prefix exists then replace refs/ with it.
2149 if pending_prefix:
2150 remote_branch = remote_branch.replace('refs/', pending_prefix)
2151 return remote_branch
2152
2153
piman@chromium.org336f9122014-09-04 02:16:55 +00002154def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002155 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2157 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002158 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159 if options.emulate_svn_auto_props:
2160 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161
2162 change_desc = None
2163
pgervais@chromium.org91141372014-01-09 23:27:20 +00002164 if options.email is not None:
2165 upload_args.extend(['--email', options.email])
2166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002168 if options.title:
2169 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002170 if options.message:
2171 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002172 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173 print ("This branch is associated with issue %s. "
2174 "Adding patch to that issue." % cl.GetIssue())
2175 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002176 if options.title:
2177 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002178 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002179 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002180 if options.reviewers or options.tbr_owners:
2181 change_desc.update_reviewers(options.reviewers,
2182 options.tbr_owners,
2183 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002184 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002185 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002186
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002187 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 print "Description is empty; aborting."
2189 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002190
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002191 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002192 if change_desc.get_reviewers():
2193 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002194 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002195 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002196 DieWithError("Must specify reviewers to send email.")
2197 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002198
2199 # We check this before applying rietveld.private assuming that in
2200 # rietveld.cc only addresses which we can send private CLs to are listed
2201 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2202 # --private is specified explicitly on the command line.
2203 if options.private:
2204 logging.warn('rietveld.cc is ignored since private flag is specified. '
2205 'You need to review and add them manually if necessary.')
2206 cc = cl.GetCCListWithoutDefault()
2207 else:
2208 cc = cl.GetCCList()
2209 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002210 if cc:
2211 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002213 if options.private or settings.GetDefaultPrivateFlag() == "True":
2214 upload_args.append('--private')
2215
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002216 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002217 if not options.find_copies:
2218 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002220 # Include the upstream repo's URL in the change -- this is useful for
2221 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002222 remote_url = cl.GetGitBaseUrlFromConfig()
2223 if not remote_url:
2224 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002225 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002226 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002227 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2228 remote_url = (cl.GetRemoteUrl() + '@'
2229 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002230 if remote_url:
2231 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002232 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002233 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2234 settings.GetPendingRefPrefix())
2235 if target_ref:
2236 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002237
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002238 # Look for dependent patchsets. See crbug.com/480453 for more details.
2239 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2240 upstream_branch = ShortBranchName(upstream_branch)
2241 if remote is '.':
2242 # A local branch is being tracked.
2243 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002244 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002245 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002246 print ('Skipping dependency patchset upload because git config '
2247 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002248 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002249 else:
2250 auth_config = auth.extract_auth_config_from_options(options)
2251 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2252 branch_cl_issue_url = branch_cl.GetIssueURL()
2253 branch_cl_issue = branch_cl.GetIssue()
2254 branch_cl_patchset = branch_cl.GetPatchset()
2255 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2256 upload_args.extend(
2257 ['--depends_on_patchset', '%s:%s' % (
2258 branch_cl_issue, branch_cl_patchset)])
2259 print
2260 print ('The current branch (%s) is tracking a local branch (%s) with '
2261 'an associated CL.') % (cl.GetBranch(), local_branch)
2262 print 'Adding %s/#ps%s as a dependency patchset.' % (
2263 branch_cl_issue_url, branch_cl_patchset)
2264 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002265
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002266 project = settings.GetProject()
2267 if project:
2268 upload_args.extend(['--project', project])
2269
rmistry@google.comef966222015-04-07 11:15:01 +00002270 if options.cq_dry_run:
2271 upload_args.extend(['--cq_dry_run'])
2272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002273 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002274 upload_args = ['upload'] + upload_args + args
2275 logging.info('upload.RealMain(%s)', upload_args)
2276 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002277 issue = int(issue)
2278 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002279 except KeyboardInterrupt:
2280 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002281 except:
2282 # If we got an exception after the user typed a description for their
2283 # change, back up the description before re-raising.
2284 if change_desc:
2285 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2286 print '\nGot exception while uploading -- saving description to %s\n' \
2287 % backup_path
2288 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002289 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002290 backup_file.close()
2291 raise
2292
2293 if not cl.GetIssue():
2294 cl.SetIssue(issue)
2295 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002296
2297 if options.use_commit_queue:
2298 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299 return 0
2300
2301
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002302def cleanup_list(l):
2303 """Fixes a list so that comma separated items are put as individual items.
2304
2305 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2306 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2307 """
2308 items = sum((i.split(',') for i in l), [])
2309 stripped_items = (i.strip() for i in items)
2310 return sorted(filter(None, stripped_items))
2311
2312
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002313@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002314def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002315 """Uploads the current changelist to codereview.
2316
2317 Can skip dependency patchset uploads for a branch by running:
2318 git config branch.branch_name.skip-deps-uploads True
2319 To unset run:
2320 git config --unset branch.branch_name.skip-deps-uploads
2321 Can also set the above globally by using the --global flag.
2322 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002323 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2324 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002325 parser.add_option('--bypass-watchlists', action='store_true',
2326 dest='bypass_watchlists',
2327 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002328 parser.add_option('-f', action='store_true', dest='force',
2329 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002330 parser.add_option('-m', dest='message', help='message for patchset')
2331 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002332 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002333 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002334 help='reviewer email addresses')
2335 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002336 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002337 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002338 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002339 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002340 parser.add_option('--emulate_svn_auto_props',
2341 '--emulate-svn-auto-props',
2342 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002343 dest="emulate_svn_auto_props",
2344 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002345 parser.add_option('-c', '--use-commit-queue', action='store_true',
2346 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002347 parser.add_option('--private', action='store_true',
2348 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002349 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002350 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002351 metavar='TARGET',
2352 help='Apply CL to remote ref TARGET. ' +
2353 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002354 parser.add_option('--squash', action='store_true',
2355 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002356 parser.add_option('--email', default=None,
2357 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002358 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2359 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002360 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2361 help='Send the patchset to do a CQ dry run right after '
2362 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002363 parser.add_option('--dependencies', action='store_true',
2364 help='Uploads CLs of all the local branches that depend on '
2365 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002366
rmistry@google.com2dd99862015-06-22 12:22:18 +00002367 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002368 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002369 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002370 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002371 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002372
sbc@chromium.org71437c02015-04-09 19:29:40 +00002373 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002374 return 1
2375
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002376 options.reviewers = cleanup_list(options.reviewers)
2377 options.cc = cleanup_list(options.cc)
2378
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002379 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002380 if args:
2381 # TODO(ukai): is it ok for gerrit case?
2382 base_branch = args[0]
2383 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002384 if cl.GetBranch() is None:
2385 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2386
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002387 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002388 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002389 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002390
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002391 # Make sure authenticated to Rietveld before running expensive hooks. It is
2392 # a fast, best efforts check. Rietveld still can reject the authentication
2393 # during the actual upload.
2394 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2395 authenticator = auth.get_authenticator_for_host(
2396 cl.GetRietveldServer(), auth_config)
2397 if not authenticator.has_cached_credentials():
2398 raise auth.LoginRequiredError(cl.GetRietveldServer())
2399
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002400 # Apply watchlists on upload.
2401 change = cl.GetChange(base_branch, None)
2402 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2403 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002404 if not options.bypass_watchlists:
2405 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002406
ukai@chromium.orge8077812012-02-03 03:41:46 +00002407 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002408 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002409 # Set the reviewer list now so that presubmit checks can access it.
2410 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002411 change_description.update_reviewers(options.reviewers,
2412 options.tbr_owners,
2413 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002414 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002415 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002416 may_prompt=not options.force,
2417 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002418 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002419 if not hook_results.should_continue():
2420 return 1
2421 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002422 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002423
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002424 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002425 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002426 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002427 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002428 print ('The last upload made from this repository was patchset #%d but '
2429 'the most recent patchset on the server is #%d.'
2430 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002431 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2432 'from another machine or branch the patch you\'re uploading now '
2433 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002434 ask_for_data('About to upload; enter to confirm.')
2435
iannucci@chromium.org79540052012-10-19 23:15:26 +00002436 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002437 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002438 return GerritUpload(options, args, cl, change)
2439 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002440 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002441 git_set_branch_value('last-upload-hash',
2442 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002443 # Run post upload hooks, if specified.
2444 if settings.GetRunPostUploadHook():
2445 presubmit_support.DoPostUploadExecuter(
2446 change,
2447 cl,
2448 settings.GetRoot(),
2449 options.verbose,
2450 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002451
rmistry@google.com2dd99862015-06-22 12:22:18 +00002452 # Upload all dependencies if specified.
2453 if options.dependencies:
2454 print
2455 print '--dependencies has been specified.'
2456 print 'All dependent local branches will be re-uploaded.'
2457 print
2458 # Remove the dependencies flag from args so that we do not end up in a
2459 # loop.
2460 orig_args.remove('--dependencies')
2461 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002462 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002463
2464
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002465def IsSubmoduleMergeCommit(ref):
2466 # When submodules are added to the repo, we expect there to be a single
2467 # non-git-svn merge commit at remote HEAD with a signature comment.
2468 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002469 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002470 return RunGit(cmd) != ''
2471
2472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002473def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002474 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002475
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002476 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 Updates changelog with metadata (e.g. pointer to review).
2478 Pushes/dcommits the code upstream.
2479 Updates review and closes.
2480 """
2481 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2482 help='bypass upload presubmit hook')
2483 parser.add_option('-m', dest='message',
2484 help="override review description")
2485 parser.add_option('-f', action='store_true', dest='force',
2486 help="force yes to questions (don't prompt)")
2487 parser.add_option('-c', dest='contributor',
2488 help="external contributor for patch (appended to " +
2489 "description and used as author for git). Should be " +
2490 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002491 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002492 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002494 auth_config = auth.extract_auth_config_from_options(options)
2495
2496 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002497
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002498 current = cl.GetBranch()
2499 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2500 if not settings.GetIsGitSvn() and remote == '.':
2501 print
2502 print 'Attempting to push branch %r into another local branch!' % current
2503 print
2504 print 'Either reparent this branch on top of origin/master:'
2505 print ' git reparent-branch --root'
2506 print
2507 print 'OR run `git rebase-update` if you think the parent branch is already'
2508 print 'committed.'
2509 print
2510 print ' Current parent: %r' % upstream_branch
2511 return 1
2512
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002513 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 # Default to merging against our best guess of the upstream branch.
2515 args = [cl.GetUpstreamBranch()]
2516
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002517 if options.contributor:
2518 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2519 print "Please provide contibutor as 'First Last <email@example.com>'"
2520 return 1
2521
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002522 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002523 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524
sbc@chromium.org71437c02015-04-09 19:29:40 +00002525 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002526 return 1
2527
2528 # This rev-list syntax means "show all commits not in my branch that
2529 # are in base_branch".
2530 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2531 base_branch]).splitlines()
2532 if upstream_commits:
2533 print ('Base branch "%s" has %d commits '
2534 'not in this branch.' % (base_branch, len(upstream_commits)))
2535 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2536 return 1
2537
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002538 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002539 svn_head = None
2540 if cmd == 'dcommit' or base_has_submodules:
2541 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2542 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002545 # If the base_head is a submodule merge commit, the first parent of the
2546 # base_head should be a git-svn commit, which is what we're interested in.
2547 base_svn_head = base_branch
2548 if base_has_submodules:
2549 base_svn_head += '^1'
2550
2551 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552 if extra_commits:
2553 print ('This branch has %d additional commits not upstreamed yet.'
2554 % len(extra_commits.splitlines()))
2555 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2556 'before attempting to %s.' % (base_branch, cmd))
2557 return 1
2558
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002559 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002560 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002561 author = None
2562 if options.contributor:
2563 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002564 hook_results = cl.RunHook(
2565 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002566 may_prompt=not options.force,
2567 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002568 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002569 if not hook_results.should_continue():
2570 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002571
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002572 # Check the tree status if the tree status URL is set.
2573 status = GetTreeStatus()
2574 if 'closed' == status:
2575 print('The tree is closed. Please wait for it to reopen. Use '
2576 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2577 return 1
2578 elif 'unknown' == status:
2579 print('Unable to determine tree status. Please verify manually and '
2580 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2581 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002582 else:
2583 breakpad.SendStack(
2584 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002585 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2586 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002587 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002588
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002589 change_desc = ChangeDescription(options.message)
2590 if not change_desc.description and cl.GetIssue():
2591 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002593 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002594 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002595 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002596 else:
2597 print 'No description set.'
2598 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2599 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002600
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002601 # Keep a separate copy for the commit message, because the commit message
2602 # contains the link to the Rietveld issue, while the Rietveld message contains
2603 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002604 # Keep a separate copy for the commit message.
2605 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002606 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002607
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002608 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002609 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002610 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002611 # after it. Add a period on a new line to circumvent this. Also add a space
2612 # before the period to make sure that Gitiles continues to correctly resolve
2613 # the URL.
2614 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002615 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002616 commit_desc.append_footer('Patch from %s.' % options.contributor)
2617
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002618 print('Description:')
2619 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002620
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002621 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002623 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002624
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002625 # We want to squash all this branch's commits into one commit with the proper
2626 # description. We do this by doing a "reset --soft" to the base branch (which
2627 # keeps the working copy the same), then dcommitting that. If origin/master
2628 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2629 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002630 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002631 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2632 # Delete the branches if they exist.
2633 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2634 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2635 result = RunGitWithCode(showref_cmd)
2636 if result[0] == 0:
2637 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638
2639 # We might be in a directory that's present in this branch but not in the
2640 # trunk. Move up to the top of the tree so that git commands that expect a
2641 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002642 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643 if rel_base_path:
2644 os.chdir(rel_base_path)
2645
2646 # Stuff our change into the merge branch.
2647 # We wrap in a try...finally block so if anything goes wrong,
2648 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002649 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002650 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002651 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002652 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002653 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002654 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002655 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002656 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002657 RunGit(
2658 [
2659 'commit', '--author', options.contributor,
2660 '-m', commit_desc.description,
2661 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002662 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002664 if base_has_submodules:
2665 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2666 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2667 RunGit(['checkout', CHERRY_PICK_BRANCH])
2668 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002669 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002670 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002671 pending_prefix = settings.GetPendingRefPrefix()
2672 if not pending_prefix or branch.startswith(pending_prefix):
2673 # If not using refs/pending/heads/* at all, or target ref is already set
2674 # to pending, then push to the target ref directly.
2675 retcode, output = RunGitWithCode(
2676 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002677 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002678 else:
2679 # Cherry-pick the change on top of pending ref and then push it.
2680 assert branch.startswith('refs/'), branch
2681 assert pending_prefix[-1] == '/', pending_prefix
2682 pending_ref = pending_prefix + branch[len('refs/'):]
2683 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002684 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002685 if retcode == 0:
2686 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687 else:
2688 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002689 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002690 'svn', 'dcommit',
2691 '-C%s' % options.similarity,
2692 '--no-rebase', '--rmdir',
2693 ]
2694 if settings.GetForceHttpsCommitUrl():
2695 # Allow forcing https commit URLs for some projects that don't allow
2696 # committing to http URLs (like Google Code).
2697 remote_url = cl.GetGitSvnRemoteUrl()
2698 if urlparse.urlparse(remote_url).scheme == 'http':
2699 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002700 cmd_args.append('--commit-url=%s' % remote_url)
2701 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002702 if 'Committed r' in output:
2703 revision = re.match(
2704 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2705 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002706 finally:
2707 # And then swap back to the original branch and clean up.
2708 RunGit(['checkout', '-q', cl.GetBranch()])
2709 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002710 if base_has_submodules:
2711 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002713 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002714 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002715 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002716
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002717 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002718 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002719 try:
2720 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2721 # We set pushed_to_pending to False, since it made it all the way to the
2722 # real ref.
2723 pushed_to_pending = False
2724 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002725 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002726
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002728 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002730 if not to_pending:
2731 if viewvc_url and revision:
2732 change_desc.append_footer(
2733 'Committed: %s%s' % (viewvc_url, revision))
2734 elif revision:
2735 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736 print ('Closing issue '
2737 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002738 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002740 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002741 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002742 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002743 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002744 if options.bypass_hooks:
2745 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2746 else:
2747 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002748 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002749 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002750
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002751 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002752 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2753 print 'The commit is in the pending queue (%s).' % pending_ref
2754 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002755 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002756 'footer.' % branch)
2757
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002758 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2759 if os.path.isfile(hook):
2760 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002761
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002762 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002763
2764
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002765def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2766 print
2767 print 'Waiting for commit to be landed on %s...' % real_ref
2768 print '(If you are impatient, you may Ctrl-C once without harm)'
2769 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2770 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2771
2772 loop = 0
2773 while True:
2774 sys.stdout.write('fetching (%d)... \r' % loop)
2775 sys.stdout.flush()
2776 loop += 1
2777
2778 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2779 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2780 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2781 for commit in commits.splitlines():
2782 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2783 print 'Found commit on %s' % real_ref
2784 return commit
2785
2786 current_rev = to_rev
2787
2788
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002789def PushToGitPending(remote, pending_ref, upstream_ref):
2790 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2791
2792 Returns:
2793 (retcode of last operation, output log of last operation).
2794 """
2795 assert pending_ref.startswith('refs/'), pending_ref
2796 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2797 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2798 code = 0
2799 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002800 max_attempts = 3
2801 attempts_left = max_attempts
2802 while attempts_left:
2803 if attempts_left != max_attempts:
2804 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2805 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002806
2807 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002808 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002809 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002810 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002811 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002812 print 'Fetch failed with exit code %d.' % code
2813 if out.strip():
2814 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002815 continue
2816
2817 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002818 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002819 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002820 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002821 if code:
2822 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002823 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2824 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002825 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2826 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002827 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002828 return code, out
2829
2830 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002831 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002832 code, out = RunGitWithCode(
2833 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2834 if code == 0:
2835 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002836 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002837 return code, out
2838
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002839 print 'Push failed with exit code %d.' % code
2840 if out.strip():
2841 print out.strip()
2842 if IsFatalPushFailure(out):
2843 print (
2844 'Fatal push error. Make sure your .netrc credentials and git '
2845 'user.email are correct and you have push access to the repo.')
2846 return code, out
2847
2848 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002849 return code, out
2850
2851
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002852def IsFatalPushFailure(push_stdout):
2853 """True if retrying push won't help."""
2854 return '(prohibited by Gerrit)' in push_stdout
2855
2856
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002857@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002859 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002861 if get_footer_svn_id():
2862 # If it looks like previous commits were mirrored with git-svn.
2863 message = """This repository appears to be a git-svn mirror, but no
2864upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2865 else:
2866 message = """This doesn't appear to be an SVN repository.
2867If your project has a true, writeable git repository, you probably want to run
2868'git cl land' instead.
2869If your project has a git mirror of an upstream SVN master, you probably need
2870to run 'git svn init'.
2871
2872Using the wrong command might cause your commit to appear to succeed, and the
2873review to be closed, without actually landing upstream. If you choose to
2874proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002875 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002876 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 return SendUpstream(parser, args, 'dcommit')
2878
2879
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002880@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002881def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002882 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002883 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884 print('This appears to be an SVN repository.')
2885 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002886 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002887 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002888 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889
2890
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002891def ParseIssueNum(arg):
2892 """Parses the issue number from args if present otherwise returns None."""
2893 if re.match(r'\d+', arg):
2894 return arg
2895 if arg.startswith('http'):
2896 return re.sub(r'.*/(\d+)/?', r'\1', arg)
2897 return None
2898
2899
2900@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002901def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002902 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903 parser.add_option('-b', dest='newbranch',
2904 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002905 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002906 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002907 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2908 help='Change to the directory DIR immediately, '
2909 'before doing anything else.')
2910 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002911 help='failed patches spew .rej files rather than '
2912 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2914 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002915 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002916 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002917 auth_config = auth.extract_auth_config_from_options(options)
2918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002919 if len(args) != 1:
2920 parser.print_help()
2921 return 1
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002922
2923 issue_arg = ParseIssueNum(args[0])
2924 # The patch URL works because ParseIssueNum won't do any substitution
2925 # as the re.sub pattern fails to match and just returns it.
2926 if issue_arg == None:
2927 parser.print_help()
2928 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002929
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002930 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002931 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002932 return 1
2933
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002934 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002935 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002936
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002937 if options.newbranch:
2938 if options.force:
2939 RunGit(['branch', '-D', options.newbranch],
2940 stderr=subprocess2.PIPE, error_ok=True)
2941 RunGit(['checkout', '-b', options.newbranch,
2942 Changelist().GetUpstreamBranch()])
2943
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002944 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002945 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002946
2947
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002948def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002949 # PatchIssue should never be called with a dirty tree. It is up to the
2950 # caller to check this, but just in case we assert here since the
2951 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002952 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002953
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002954 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002956 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002957 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002958 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002959 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002961 # Assume it's a URL to the patch. Default to https.
2962 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002963 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002964 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 DieWithError('Must pass an issue ID or full URL for '
2966 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002967 issue = int(match.group(2))
2968 cl = Changelist(issue=issue, auth_config=auth_config)
2969 cl.rietveld_server = match.group(1)
2970 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002971 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973 # Switch up to the top-level directory, if necessary, in preparation for
2974 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002975 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976 if top:
2977 os.chdir(top)
2978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002979 # Git patches have a/ at the beginning of source paths. We strip that out
2980 # with a sed script rather than the -p flag to patch so we can feed either
2981 # Git or svn-style patches into the same apply command.
2982 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002983 try:
2984 patch_data = subprocess2.check_output(
2985 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2986 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002987 DieWithError('Git patch mungling failed.')
2988 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002989
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002990 # We use "git apply" to apply the patch instead of "patch" so that we can
2991 # pick up file adds.
2992 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002993 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002994 if directory:
2995 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002996 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002997 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002998 elif IsGitVersionAtLeast('1.7.12'):
2999 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003000 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003001 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003002 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003003 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003004 print 'Failed to apply the patch'
3005 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006
3007 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003008 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003009 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3010 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003011 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3012 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003013 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003014 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003015 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003016 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003017 else:
3018 print "Patch applied to index."
3019 return 0
3020
3021
3022def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003023 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003024 # Provide a wrapper for git svn rebase to help avoid accidental
3025 # git svn dcommit.
3026 # It's the only command that doesn't use parser at all since we just defer
3027 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003028
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003029 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003030
3031
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003032def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033 """Fetches the tree status and returns either 'open', 'closed',
3034 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003035 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003036 if url:
3037 status = urllib2.urlopen(url).read().lower()
3038 if status.find('closed') != -1 or status == '0':
3039 return 'closed'
3040 elif status.find('open') != -1 or status == '1':
3041 return 'open'
3042 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003043 return 'unset'
3044
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046def GetTreeStatusReason():
3047 """Fetches the tree status from a json url and returns the message
3048 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003049 url = settings.GetTreeStatusUrl()
3050 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051 connection = urllib2.urlopen(json_url)
3052 status = json.loads(connection.read())
3053 connection.close()
3054 return status['message']
3055
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003056
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003057def GetBuilderMaster(bot_list):
3058 """For a given builder, fetch the master from AE if available."""
3059 map_url = 'https://builders-map.appspot.com/'
3060 try:
3061 master_map = json.load(urllib2.urlopen(map_url))
3062 except urllib2.URLError as e:
3063 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3064 (map_url, e))
3065 except ValueError as e:
3066 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3067 if not master_map:
3068 return None, 'Failed to build master map.'
3069
3070 result_master = ''
3071 for bot in bot_list:
3072 builder = bot.split(':', 1)[0]
3073 master_list = master_map.get(builder, [])
3074 if not master_list:
3075 return None, ('No matching master for builder %s.' % builder)
3076 elif len(master_list) > 1:
3077 return None, ('The builder name %s exists in multiple masters %s.' %
3078 (builder, master_list))
3079 else:
3080 cur_master = master_list[0]
3081 if not result_master:
3082 result_master = cur_master
3083 elif result_master != cur_master:
3084 return None, 'The builders do not belong to the same master.'
3085 return result_master, None
3086
3087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003089 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003090 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003091 status = GetTreeStatus()
3092 if 'unset' == status:
3093 print 'You must configure your tree status URL by running "git cl config".'
3094 return 2
3095
3096 print "The tree is %s" % status
3097 print
3098 print GetTreeStatusReason()
3099 if status != 'open':
3100 return 1
3101 return 0
3102
3103
maruel@chromium.org15192402012-09-06 12:38:29 +00003104def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003105 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003106 group = optparse.OptionGroup(parser, "Try job options")
3107 group.add_option(
3108 "-b", "--bot", action="append",
3109 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3110 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003111 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003112 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003113 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003114 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003115 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003116 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003117 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003118 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003119 "-r", "--revision",
3120 help="Revision to use for the try job; default: the "
3121 "revision will be determined by the try server; see "
3122 "its waterfall for more info")
3123 group.add_option(
3124 "-c", "--clobber", action="store_true", default=False,
3125 help="Force a clobber before building; e.g. don't do an "
3126 "incremental build")
3127 group.add_option(
3128 "--project",
3129 help="Override which project to use. Projects are defined "
3130 "server-side to define what default bot set to use")
3131 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003132 "-p", "--property", dest="properties", action="append", default=[],
3133 help="Specify generic properties in the form -p key1=value1 -p "
3134 "key2=value2 etc (buildbucket only). The value will be treated as "
3135 "json if decodable, or as string otherwise.")
3136 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003137 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003138 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003139 "--use-rietveld", action="store_true", default=False,
3140 help="Use Rietveld to trigger try jobs.")
3141 group.add_option(
3142 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3143 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003144 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003145 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003146 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003147 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003148
machenbach@chromium.org45453142015-09-15 08:45:22 +00003149 if options.use_rietveld and options.properties:
3150 parser.error('Properties can only be specified with buildbucket')
3151
3152 # Make sure that all properties are prop=value pairs.
3153 bad_params = [x for x in options.properties if '=' not in x]
3154 if bad_params:
3155 parser.error('Got properties with missing "=": %s' % bad_params)
3156
maruel@chromium.org15192402012-09-06 12:38:29 +00003157 if args:
3158 parser.error('Unknown arguments: %s' % args)
3159
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003160 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003161 if not cl.GetIssue():
3162 parser.error('Need to upload first')
3163
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003164 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003165 if props.get('closed'):
3166 parser.error('Cannot send tryjobs for a closed CL')
3167
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003168 if props.get('private'):
3169 parser.error('Cannot use trybots with private issue')
3170
maruel@chromium.org15192402012-09-06 12:38:29 +00003171 if not options.name:
3172 options.name = cl.GetBranch()
3173
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003174 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003175 options.master, err_msg = GetBuilderMaster(options.bot)
3176 if err_msg:
3177 parser.error('Tryserver master cannot be found because: %s\n'
3178 'Please manually specify the tryserver master'
3179 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003180
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003181 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003182 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003183 if not options.bot:
3184 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003185
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003186 # Get try masters from PRESUBMIT.py files.
3187 masters = presubmit_support.DoGetTryMasters(
3188 change,
3189 change.LocalPaths(),
3190 settings.GetRoot(),
3191 None,
3192 None,
3193 options.verbose,
3194 sys.stdout)
3195 if masters:
3196 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003197
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003198 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3199 options.bot = presubmit_support.DoGetTrySlaves(
3200 change,
3201 change.LocalPaths(),
3202 settings.GetRoot(),
3203 None,
3204 None,
3205 options.verbose,
3206 sys.stdout)
3207 if not options.bot:
3208 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003209
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003210 builders_and_tests = {}
3211 # TODO(machenbach): The old style command-line options don't support
3212 # multiple try masters yet.
3213 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3214 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3215
3216 for bot in old_style:
3217 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003218 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003219 elif ',' in bot:
3220 parser.error('Specify one bot per --bot flag')
3221 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003222 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003223
3224 for bot, tests in new_style:
3225 builders_and_tests.setdefault(bot, []).extend(tests)
3226
3227 # Return a master map with one master to be backwards compatible. The
3228 # master name defaults to an empty string, which will cause the master
3229 # not to be set on rietveld (deprecated).
3230 return {options.master: builders_and_tests}
3231
3232 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003233
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003234 for builders in masters.itervalues():
3235 if any('triggered' in b for b in builders):
3236 print >> sys.stderr, (
3237 'ERROR You are trying to send a job to a triggered bot. This type of'
3238 ' bot requires an\ninitial job from a parent (usually a builder). '
3239 'Instead send your job to the parent.\n'
3240 'Bot list: %s' % builders)
3241 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003242
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003243 patchset = cl.GetMostRecentPatchset()
3244 if patchset and patchset != cl.GetPatchset():
3245 print(
3246 '\nWARNING Mismatch between local config and server. Did a previous '
3247 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3248 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003249 if options.luci:
3250 trigger_luci_job(cl, masters, options)
3251 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003252 try:
3253 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3254 except BuildbucketResponseException as ex:
3255 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003256 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003257 except Exception as e:
3258 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3259 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3260 e, stacktrace)
3261 return 1
3262 else:
3263 try:
3264 cl.RpcServer().trigger_distributed_try_jobs(
3265 cl.GetIssue(), patchset, options.name, options.clobber,
3266 options.revision, masters)
3267 except urllib2.HTTPError as e:
3268 if e.code == 404:
3269 print('404 from rietveld; '
3270 'did you mean to use "git try" instead of "git cl try"?')
3271 return 1
3272 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003273
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003274 for (master, builders) in sorted(masters.iteritems()):
3275 if master:
3276 print 'Master: %s' % master
3277 length = max(len(builder) for builder in builders)
3278 for builder in sorted(builders):
3279 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003280 return 0
3281
3282
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003283@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003285 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003286 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003287 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003288 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003290 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003291 if args:
3292 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003293 branch = cl.GetBranch()
3294 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003295 cl = Changelist()
3296 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003297
3298 # Clear configured merge-base, if there is one.
3299 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003300 else:
3301 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 return 0
3303
3304
thestig@chromium.org00858c82013-12-02 23:08:03 +00003305def CMDweb(parser, args):
3306 """Opens the current CL in the web browser."""
3307 _, args = parser.parse_args(args)
3308 if args:
3309 parser.error('Unrecognized args: %s' % ' '.join(args))
3310
3311 issue_url = Changelist().GetIssueURL()
3312 if not issue_url:
3313 print >> sys.stderr, 'ERROR No issue to open'
3314 return 1
3315
3316 webbrowser.open(issue_url)
3317 return 0
3318
3319
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003320def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003321 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003322 auth.add_auth_options(parser)
3323 options, args = parser.parse_args(args)
3324 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003325 if args:
3326 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003327 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003328 props = cl.GetIssueProperties()
3329 if props.get('private'):
3330 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003331 cl.SetFlag('commit', '1')
3332 return 0
3333
3334
groby@chromium.org411034a2013-02-26 15:12:01 +00003335def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003336 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003337 auth.add_auth_options(parser)
3338 options, args = parser.parse_args(args)
3339 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003340 if args:
3341 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003342 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003343 # Ensure there actually is an issue to close.
3344 cl.GetDescription()
3345 cl.CloseIssue()
3346 return 0
3347
3348
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003349def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003350 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003351 auth.add_auth_options(parser)
3352 options, args = parser.parse_args(args)
3353 auth_config = auth.extract_auth_config_from_options(options)
3354 if args:
3355 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003356
3357 # Uncommitted (staged and unstaged) changes will be destroyed by
3358 # "git reset --hard" if there are merging conflicts in PatchIssue().
3359 # Staged changes would be committed along with the patch from last
3360 # upload, hence counted toward the "last upload" side in the final
3361 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003362 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003363 return 1
3364
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003365 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003366 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003367 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003368 if not issue:
3369 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003370 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003371 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003372
3373 # Create a new branch based on the merge-base
3374 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3375 try:
3376 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003377 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003378 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003379 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003380 return rtn
3381
wychen@chromium.org06928532015-02-03 02:11:29 +00003382 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003383 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003384 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003385 finally:
3386 RunGit(['checkout', '-q', branch])
3387 RunGit(['branch', '-D', TMP_BRANCH])
3388
3389 return 0
3390
3391
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003392def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003393 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003394 parser.add_option(
3395 '--no-color',
3396 action='store_true',
3397 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003398 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003399 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003400 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003401
3402 author = RunGit(['config', 'user.email']).strip() or None
3403
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003404 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003405
3406 if args:
3407 if len(args) > 1:
3408 parser.error('Unknown args')
3409 base_branch = args[0]
3410 else:
3411 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003412 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003413
3414 change = cl.GetChange(base_branch, None)
3415 return owners_finder.OwnersFinder(
3416 [f.LocalPath() for f in
3417 cl.GetChange(base_branch, None).AffectedFiles()],
3418 change.RepositoryRoot(), author,
3419 fopen=file, os_path=os.path, glob=glob.glob,
3420 disable_color=options.no_color).run()
3421
3422
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003423def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3424 """Generates a diff command."""
3425 # Generate diff for the current branch's changes.
3426 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3427 upstream_commit, '--' ]
3428
3429 if args:
3430 for arg in args:
3431 if os.path.isdir(arg):
3432 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3433 elif os.path.isfile(arg):
3434 diff_cmd.append(arg)
3435 else:
3436 DieWithError('Argument "%s" is not a file or a directory' % arg)
3437 else:
3438 diff_cmd.extend('*' + ext for ext in extensions)
3439
3440 return diff_cmd
3441
3442
enne@chromium.org555cfe42014-01-29 18:21:39 +00003443@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003444def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003445 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003446 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003447 parser.add_option('--full', action='store_true',
3448 help='Reformat the full content of all touched files')
3449 parser.add_option('--dry-run', action='store_true',
3450 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003451 parser.add_option('--python', action='store_true',
3452 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003453 parser.add_option('--diff', action='store_true',
3454 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003455 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003456
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003457 # git diff generates paths against the root of the repository. Change
3458 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003459 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003460 if rel_base_path:
3461 os.chdir(rel_base_path)
3462
digit@chromium.org29e47272013-05-17 17:01:46 +00003463 # Grab the merge-base commit, i.e. the upstream commit of the current
3464 # branch when it was created or the last time it was rebased. This is
3465 # to cover the case where the user may have called "git fetch origin",
3466 # moving the origin branch to a newer commit, but hasn't rebased yet.
3467 upstream_commit = None
3468 cl = Changelist()
3469 upstream_branch = cl.GetUpstreamBranch()
3470 if upstream_branch:
3471 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3472 upstream_commit = upstream_commit.strip()
3473
3474 if not upstream_commit:
3475 DieWithError('Could not find base commit for this branch. '
3476 'Are you in detached state?')
3477
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003478 if opts.full:
3479 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003480 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003481 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003482 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003483 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003484
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003485 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003486 diff_output = RunGit(diff_cmd)
3487
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003488 top_dir = os.path.normpath(
3489 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3490
3491 # Locate the clang-format binary in the checkout
3492 try:
3493 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3494 except clang_format.NotFoundError, e:
3495 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003496
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003497 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3498 # formatted. This is used to block during the presubmit.
3499 return_value = 0
3500
digit@chromium.org29e47272013-05-17 17:01:46 +00003501 if opts.full:
3502 # diff_output is a list of files to send to clang-format.
3503 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003504 if files:
3505 cmd = [clang_format_tool]
3506 if not opts.dry_run and not opts.diff:
3507 cmd.append('-i')
3508 stdout = RunCommand(cmd + files, cwd=top_dir)
3509 if opts.diff:
3510 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003511 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003512 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003513 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003514 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003515 try:
3516 script = clang_format.FindClangFormatScriptInChromiumTree(
3517 'clang-format-diff.py')
3518 except clang_format.NotFoundError, e:
3519 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003520
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003521 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003522 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003523 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003524
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003525 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003526 if opts.diff:
3527 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003528 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003529 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003530
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003531 # Similar code to above, but using yapf on .py files rather than clang-format
3532 # on C/C++ files
3533 if opts.python:
3534 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3535 diff_output = RunGit(diff_cmd)
3536 yapf_tool = gclient_utils.FindExecutable('yapf')
3537 if yapf_tool is None:
3538 DieWithError('yapf not found in PATH')
3539
3540 if opts.full:
3541 files = diff_output.splitlines()
3542 if files:
3543 cmd = [yapf_tool]
3544 if not opts.dry_run and not opts.diff:
3545 cmd.append('-i')
3546 stdout = RunCommand(cmd + files, cwd=top_dir)
3547 if opts.diff:
3548 sys.stdout.write(stdout)
3549 else:
3550 # TODO(sbc): yapf --lines mode still has some issues.
3551 # https://github.com/google/yapf/issues/154
3552 DieWithError('--python currently only works with --full')
3553
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003554 # Build a diff command that only operates on dart files. dart's formatter
3555 # does not have the nice property of only operating on modified chunks, so
3556 # hard code full.
3557 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3558 args, ['.dart'])
3559 dart_diff_output = RunGit(dart_diff_cmd)
3560 if dart_diff_output:
3561 try:
3562 command = [dart_format.FindDartFmtToolInChromiumTree()]
3563 if not opts.dry_run and not opts.diff:
3564 command.append('-w')
3565 command.extend(dart_diff_output.splitlines())
3566
3567 stdout = RunCommand(command, cwd=top_dir, env=env)
3568 if opts.dry_run and stdout:
3569 return_value = 2
3570 except dart_format.NotFoundError as e:
3571 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3572 'this checkout.')
3573
3574 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003575
3576
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003577@subcommand.usage('<codereview url or issue id>')
3578def CMDcheckout(parser, args):
3579 """Checks out a branch associated with a given Rietveld issue."""
3580 _, args = parser.parse_args(args)
3581
3582 if len(args) != 1:
3583 parser.print_help()
3584 return 1
3585
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003586 target_issue = ParseIssueNum(args[0])
3587 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003588 parser.print_help()
3589 return 1
3590
3591 key_and_issues = [x.split() for x in RunGit(
3592 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3593 .splitlines()]
3594 branches = []
3595 for key, issue in key_and_issues:
3596 if issue == target_issue:
3597 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3598
3599 if len(branches) == 0:
3600 print 'No branch found for issue %s.' % target_issue
3601 return 1
3602 if len(branches) == 1:
3603 RunGit(['checkout', branches[0]])
3604 else:
3605 print 'Multiple branches match issue %s:' % target_issue
3606 for i in range(len(branches)):
3607 print '%d: %s' % (i, branches[i])
3608 which = raw_input('Choose by index: ')
3609 try:
3610 RunGit(['checkout', branches[int(which)]])
3611 except (IndexError, ValueError):
3612 print 'Invalid selection, not checking out any branch.'
3613 return 1
3614
3615 return 0
3616
3617
maruel@chromium.org29404b52014-09-08 22:58:00 +00003618def CMDlol(parser, args):
3619 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003620 print zlib.decompress(base64.b64decode(
3621 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3622 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3623 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3624 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003625 return 0
3626
3627
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003628class OptionParser(optparse.OptionParser):
3629 """Creates the option parse and add --verbose support."""
3630 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003631 optparse.OptionParser.__init__(
3632 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003633 self.add_option(
3634 '-v', '--verbose', action='count', default=0,
3635 help='Use 2 times for more debugging info')
3636
3637 def parse_args(self, args=None, values=None):
3638 options, args = optparse.OptionParser.parse_args(self, args, values)
3639 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3640 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3641 return options, args
3642
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003643
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003645 if sys.hexversion < 0x02060000:
3646 print >> sys.stderr, (
3647 '\nYour python version %s is unsupported, please upgrade.\n' %
3648 sys.version.split(' ', 1)[0])
3649 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003650
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003651 # Reload settings.
3652 global settings
3653 settings = Settings()
3654
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003655 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003656 dispatcher = subcommand.CommandDispatcher(__name__)
3657 try:
3658 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003659 except auth.AuthenticationError as e:
3660 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003661 except urllib2.HTTPError, e:
3662 if e.code != 500:
3663 raise
3664 DieWithError(
3665 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3666 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003667 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668
3669
3670if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003671 # These affect sys.stdout so do it outside of main() to simplify mocks in
3672 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003673 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003674 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003675 try:
3676 sys.exit(main(sys.argv[1:]))
3677 except KeyboardInterrupt:
3678 sys.stderr.write('interrupted\n')
3679 sys.exit(1)