blob: b981b8f76bda37f9b84ebd62f5e83f02cdf7ba06 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000042from luci_hacks import trigger_luci_job as luci_trigger
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
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()):
248 for builder in sorted(builders_and_tests.iterkeys()):
249 luci_trigger.trigger(
250 builder, 'HEAD', issue, patchset, issue_props['project'])
251
252
machenbach@chromium.org45453142015-09-15 08:45:22 +0000253def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000254 rietveld_url = settings.GetDefaultServerUrl()
255 rietveld_host = urlparse.urlparse(rietveld_url).hostname
256 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
257 http = authenticator.authorize(httplib2.Http())
258 http.force_exception_to_status_code = True
259 issue_props = changelist.GetIssueProperties()
260 issue = changelist.GetIssue()
261 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000262 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000263
264 buildbucket_put_url = (
265 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000266 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000267 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
268 hostname=rietveld_host,
269 issue=issue,
270 patch=patchset)
271
272 batch_req_body = {'builds': []}
273 print_text = []
274 print_text.append('Tried jobs on:')
275 for master, builders_and_tests in sorted(masters.iteritems()):
276 print_text.append('Master: %s' % master)
277 bucket = _prefix_master(master)
278 for builder, tests in sorted(builders_and_tests.iteritems()):
279 print_text.append(' %s: %s' % (builder, tests))
280 parameters = {
281 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000282 'changes': [{
283 'author': {'email': issue_props['owner_email']},
284 'revision': options.revision,
285 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000286 'properties': {
287 'category': category,
288 'issue': issue,
289 'master': master,
290 'patch_project': issue_props['project'],
291 'patch_storage': 'rietveld',
292 'patchset': patchset,
293 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000294 'rietveld': rietveld_url,
295 'testfilter': tests,
296 },
297 }
machenbach@chromium.org45453142015-09-15 08:45:22 +0000298 if properties:
299 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000300 if options.clobber:
301 parameters['properties']['clobber'] = True
302 batch_req_body['builds'].append(
303 {
304 'bucket': bucket,
305 'parameters_json': json.dumps(parameters),
306 'tags': ['builder:%s' % builder,
307 'buildset:%s' % buildset,
308 'master:%s' % master,
309 'user_agent:git_cl_try']
310 }
311 )
312
313 for try_count in xrange(3):
314 response, content = http.request(
315 buildbucket_put_url,
316 'PUT',
317 body=json.dumps(batch_req_body),
318 headers={'Content-Type': 'application/json'},
319 )
320 content_json = None
321 try:
322 content_json = json.loads(content)
323 except ValueError:
324 pass
325
326 # Buildbucket could return an error even if status==200.
327 if content_json and content_json.get('error'):
328 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
329 content_json['error'].get('code', ''),
330 content_json['error'].get('reason', ''),
331 content_json['error'].get('message', ''))
332 raise BuildbucketResponseException(msg)
333
334 if response.status == 200:
335 if not content_json:
336 raise BuildbucketResponseException(
337 'Buildbucket returns invalid json content: %s.\n'
338 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
339 content)
340 break
341 if response.status < 500 or try_count >= 2:
342 raise httplib2.HttpLib2Error(content)
343
344 # status >= 500 means transient failures.
345 logging.debug('Transient errors when triggering tryjobs. Will retry.')
346 time.sleep(0.5 + 1.5*try_count)
347
348 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000349
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000351def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
352 """Return the corresponding git ref if |base_url| together with |glob_spec|
353 matches the full |url|.
354
355 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
356 """
357 fetch_suburl, as_ref = glob_spec.split(':')
358 if allow_wildcards:
359 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
360 if glob_match:
361 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
362 # "branches/{472,597,648}/src:refs/remotes/svn/*".
363 branch_re = re.escape(base_url)
364 if glob_match.group(1):
365 branch_re += '/' + re.escape(glob_match.group(1))
366 wildcard = glob_match.group(2)
367 if wildcard == '*':
368 branch_re += '([^/]*)'
369 else:
370 # Escape and replace surrounding braces with parentheses and commas
371 # with pipe symbols.
372 wildcard = re.escape(wildcard)
373 wildcard = re.sub('^\\\\{', '(', wildcard)
374 wildcard = re.sub('\\\\,', '|', wildcard)
375 wildcard = re.sub('\\\\}$', ')', wildcard)
376 branch_re += wildcard
377 if glob_match.group(3):
378 branch_re += re.escape(glob_match.group(3))
379 match = re.match(branch_re, url)
380 if match:
381 return re.sub('\*$', match.group(1), as_ref)
382
383 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
384 if fetch_suburl:
385 full_url = base_url + '/' + fetch_suburl
386 else:
387 full_url = base_url
388 if full_url == url:
389 return as_ref
390 return None
391
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000392
iannucci@chromium.org79540052012-10-19 23:15:26 +0000393def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000394 """Prints statistics about the change to the user."""
395 # --no-ext-diff is broken in some versions of Git, so try to work around
396 # this by overriding the environment (but there is still a problem if the
397 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000398 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000399 if 'GIT_EXTERNAL_DIFF' in env:
400 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000401
402 if find_copies:
403 similarity_options = ['--find-copies-harder', '-l100000',
404 '-C%s' % similarity]
405 else:
406 similarity_options = ['-M%s' % similarity]
407
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000408 try:
409 stdout = sys.stdout.fileno()
410 except AttributeError:
411 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000412 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000413 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000414 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000415 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000416
417
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000418class BuildbucketResponseException(Exception):
419 pass
420
421
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422class Settings(object):
423 def __init__(self):
424 self.default_server = None
425 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000426 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 self.is_git_svn = None
428 self.svn_branch = None
429 self.tree_status_url = None
430 self.viewvc_url = None
431 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000432 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000433 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000434 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000435 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000436 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437
438 def LazyUpdateIfNeeded(self):
439 """Updates the settings from a codereview.settings file, if available."""
440 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000441 # The only value that actually changes the behavior is
442 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000443 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000444 error_ok=True
445 ).strip().lower()
446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000448 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000450 # set updated to True to avoid infinite calling loop
451 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000452 self.updated = True
453 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 self.updated = True
455
456 def GetDefaultServerUrl(self, error_ok=False):
457 if not self.default_server:
458 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000459 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000460 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461 if error_ok:
462 return self.default_server
463 if not self.default_server:
464 error_message = ('Could not find settings file. You must configure '
465 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000466 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000467 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468 return self.default_server
469
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000470 @staticmethod
471 def GetRelativeRoot():
472 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000475 if self.root is None:
476 self.root = os.path.abspath(self.GetRelativeRoot())
477 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478
479 def GetIsGitSvn(self):
480 """Return true if this repo looks like it's using git-svn."""
481 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000482 if self.GetPendingRefPrefix():
483 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
484 self.is_git_svn = False
485 else:
486 # If you have any "svn-remote.*" config keys, we think you're using svn.
487 self.is_git_svn = RunGitWithCode(
488 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000489 return self.is_git_svn
490
491 def GetSVNBranch(self):
492 if self.svn_branch is None:
493 if not self.GetIsGitSvn():
494 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
495
496 # Try to figure out which remote branch we're based on.
497 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000498 # 1) iterate through our branch history and find the svn URL.
499 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000500
501 # regexp matching the git-svn line that contains the URL.
502 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
503
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000504 # We don't want to go through all of history, so read a line from the
505 # pipe at a time.
506 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000507 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000508 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
509 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000510 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000511 for line in proc.stdout:
512 match = git_svn_re.match(line)
513 if match:
514 url = match.group(1)
515 proc.stdout.close() # Cut pipe.
516 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000517
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000518 if url:
519 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
520 remotes = RunGit(['config', '--get-regexp',
521 r'^svn-remote\..*\.url']).splitlines()
522 for remote in remotes:
523 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000524 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000525 remote = match.group(1)
526 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000527 rewrite_root = RunGit(
528 ['config', 'svn-remote.%s.rewriteRoot' % remote],
529 error_ok=True).strip()
530 if rewrite_root:
531 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000532 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000533 ['config', 'svn-remote.%s.fetch' % remote],
534 error_ok=True).strip()
535 if fetch_spec:
536 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
537 if self.svn_branch:
538 break
539 branch_spec = RunGit(
540 ['config', 'svn-remote.%s.branches' % remote],
541 error_ok=True).strip()
542 if branch_spec:
543 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
544 if self.svn_branch:
545 break
546 tag_spec = RunGit(
547 ['config', 'svn-remote.%s.tags' % remote],
548 error_ok=True).strip()
549 if tag_spec:
550 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
551 if self.svn_branch:
552 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000553
554 if not self.svn_branch:
555 DieWithError('Can\'t guess svn branch -- try specifying it on the '
556 'command line')
557
558 return self.svn_branch
559
560 def GetTreeStatusUrl(self, error_ok=False):
561 if not self.tree_status_url:
562 error_message = ('You must configure your tree status URL by running '
563 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000564 self.tree_status_url = self._GetRietveldConfig(
565 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000566 return self.tree_status_url
567
568 def GetViewVCUrl(self):
569 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000570 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 return self.viewvc_url
572
rmistry@google.com90752582014-01-14 21:04:50 +0000573 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000574 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000575
rmistry@google.com78948ed2015-07-08 23:09:57 +0000576 def GetIsSkipDependencyUpload(self, branch_name):
577 """Returns true if specified branch should skip dep uploads."""
578 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
579 error_ok=True)
580
rmistry@google.com5626a922015-02-26 14:03:30 +0000581 def GetRunPostUploadHook(self):
582 run_post_upload_hook = self._GetRietveldConfig(
583 'run-post-upload-hook', error_ok=True)
584 return run_post_upload_hook == "True"
585
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000586 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000587 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000588
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000589 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000590 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000591
ukai@chromium.orge8077812012-02-03 03:41:46 +0000592 def GetIsGerrit(self):
593 """Return true if this repo is assosiated with gerrit code review system."""
594 if self.is_gerrit is None:
595 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
596 return self.is_gerrit
597
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000598 def GetGitEditor(self):
599 """Return the editor specified in the git config, or None if none is."""
600 if self.git_editor is None:
601 self.git_editor = self._GetConfig('core.editor', error_ok=True)
602 return self.git_editor or None
603
thestig@chromium.org44202a22014-03-11 19:22:18 +0000604 def GetLintRegex(self):
605 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
606 DEFAULT_LINT_REGEX)
607
608 def GetLintIgnoreRegex(self):
609 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
610 DEFAULT_LINT_IGNORE_REGEX)
611
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000612 def GetProject(self):
613 if not self.project:
614 self.project = self._GetRietveldConfig('project', error_ok=True)
615 return self.project
616
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000617 def GetForceHttpsCommitUrl(self):
618 if not self.force_https_commit_url:
619 self.force_https_commit_url = self._GetRietveldConfig(
620 'force-https-commit-url', error_ok=True)
621 return self.force_https_commit_url
622
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000623 def GetPendingRefPrefix(self):
624 if not self.pending_ref_prefix:
625 self.pending_ref_prefix = self._GetRietveldConfig(
626 'pending-ref-prefix', error_ok=True)
627 return self.pending_ref_prefix
628
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000629 def _GetRietveldConfig(self, param, **kwargs):
630 return self._GetConfig('rietveld.' + param, **kwargs)
631
rmistry@google.com78948ed2015-07-08 23:09:57 +0000632 def _GetBranchConfig(self, branch_name, param, **kwargs):
633 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 def _GetConfig(self, param, **kwargs):
636 self.LazyUpdateIfNeeded()
637 return RunGit(['config', param], **kwargs).strip()
638
639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640def ShortBranchName(branch):
641 """Convert a name like 'refs/heads/foo' to just 'foo'."""
642 return branch.replace('refs/heads/', '')
643
644
645class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000646 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000648 global settings
649 if not settings:
650 # Happens when git_cl.py is used as a utility library.
651 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 settings.GetDefaultServerUrl()
653 self.branchref = branchref
654 if self.branchref:
655 self.branch = ShortBranchName(self.branchref)
656 else:
657 self.branch = None
658 self.rietveld_server = None
659 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000660 self.lookedup_issue = False
661 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662 self.has_description = False
663 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000666 self.cc = None
667 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000668 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000669 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000670 self._remote = None
671 self._rpc_server = None
672
673 @property
674 def auth_config(self):
675 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000676
677 def GetCCList(self):
678 """Return the users cc'd on this CL.
679
680 Return is a string suitable for passing to gcl with the --cc flag.
681 """
682 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000683 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000684 more_cc = ','.join(self.watchers)
685 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
686 return self.cc
687
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000688 def GetCCListWithoutDefault(self):
689 """Return the users cc'd on this CL excluding default ones."""
690 if self.cc is None:
691 self.cc = ','.join(self.watchers)
692 return self.cc
693
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000694 def SetWatchers(self, watchers):
695 """Set the list of email addresses that should be cc'd based on the changed
696 files in this CL.
697 """
698 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000699
700 def GetBranch(self):
701 """Returns the short branch name, e.g. 'master'."""
702 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000703 branchref = RunGit(['symbolic-ref', 'HEAD'],
704 stderr=subprocess2.VOID, error_ok=True).strip()
705 if not branchref:
706 return None
707 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 self.branch = ShortBranchName(self.branchref)
709 return self.branch
710
711 def GetBranchRef(self):
712 """Returns the full branch name, e.g. 'refs/heads/master'."""
713 self.GetBranch() # Poke the lazy loader.
714 return self.branchref
715
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000716 @staticmethod
717 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000718 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 e.g. 'origin', 'refs/heads/master'
720 """
721 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
723 error_ok=True).strip()
724 if upstream_branch:
725 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
726 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000727 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
728 error_ok=True).strip()
729 if upstream_branch:
730 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000732 # Fall back on trying a git-svn upstream branch.
733 if settings.GetIsGitSvn():
734 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000736 # Else, try to guess the origin remote.
737 remote_branches = RunGit(['branch', '-r']).split()
738 if 'origin/master' in remote_branches:
739 # Fall back on origin/master if it exits.
740 remote = 'origin'
741 upstream_branch = 'refs/heads/master'
742 elif 'origin/trunk' in remote_branches:
743 # Fall back on origin/trunk if it exists. Generally a shared
744 # git-svn clone
745 remote = 'origin'
746 upstream_branch = 'refs/heads/trunk'
747 else:
748 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749Either pass complete "git diff"-style arguments, like
750 git cl upload origin/master
751or verify this branch is set up to track another (via the --track argument to
752"git checkout -b ...").""")
753
754 return remote, upstream_branch
755
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000756 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000757 upstream_branch = self.GetUpstreamBranch()
758 if not BranchExists(upstream_branch):
759 DieWithError('The upstream for the current branch (%s) does not exist '
760 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000761 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000762 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 def GetUpstreamBranch(self):
765 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000766 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000768 upstream_branch = upstream_branch.replace('refs/heads/',
769 'refs/remotes/%s/' % remote)
770 upstream_branch = upstream_branch.replace('refs/branch-heads/',
771 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 self.upstream_branch = upstream_branch
773 return self.upstream_branch
774
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000775 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000776 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000777 remote, branch = None, self.GetBranch()
778 seen_branches = set()
779 while branch not in seen_branches:
780 seen_branches.add(branch)
781 remote, branch = self.FetchUpstreamTuple(branch)
782 branch = ShortBranchName(branch)
783 if remote != '.' or branch.startswith('refs/remotes'):
784 break
785 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000786 remotes = RunGit(['remote'], error_ok=True).split()
787 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000788 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000789 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000790 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000791 logging.warning('Could not determine which remote this change is '
792 'associated with, so defaulting to "%s". This may '
793 'not be what you want. You may prevent this message '
794 'by running "git svn info" as documented here: %s',
795 self._remote,
796 GIT_INSTRUCTIONS_URL)
797 else:
798 logging.warn('Could not determine which remote this change is '
799 'associated with. You may prevent this message by '
800 'running "git svn info" as documented here: %s',
801 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000802 branch = 'HEAD'
803 if branch.startswith('refs/remotes'):
804 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000805 elif branch.startswith('refs/branch-heads/'):
806 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000807 else:
808 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000809 return self._remote
810
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000811 def GitSanityChecks(self, upstream_git_obj):
812 """Checks git repo status and ensures diff is from local commits."""
813
sbc@chromium.org79706062015-01-14 21:18:12 +0000814 if upstream_git_obj is None:
815 if self.GetBranch() is None:
816 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000817 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000818 else:
819 print >> sys.stderr, (
820 'ERROR: no upstream branch')
821 return False
822
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000823 # Verify the commit we're diffing against is in our current branch.
824 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
825 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
826 if upstream_sha != common_ancestor:
827 print >> sys.stderr, (
828 'ERROR: %s is not in the current branch. You may need to rebase '
829 'your tracking branch' % upstream_sha)
830 return False
831
832 # List the commits inside the diff, and verify they are all local.
833 commits_in_diff = RunGit(
834 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
835 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
836 remote_branch = remote_branch.strip()
837 if code != 0:
838 _, remote_branch = self.GetRemoteBranch()
839
840 commits_in_remote = RunGit(
841 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
842
843 common_commits = set(commits_in_diff) & set(commits_in_remote)
844 if common_commits:
845 print >> sys.stderr, (
846 'ERROR: Your diff contains %d commits already in %s.\n'
847 'Run "git log --oneline %s..HEAD" to get a list of commits in '
848 'the diff. If you are using a custom git flow, you can override'
849 ' the reference used for this check with "git config '
850 'gitcl.remotebranch <git-ref>".' % (
851 len(common_commits), remote_branch, upstream_git_obj))
852 return False
853 return True
854
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000855 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000856 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000857
858 Returns None if it is not set.
859 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000860 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
861 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000862
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000863 def GetGitSvnRemoteUrl(self):
864 """Return the configured git-svn remote URL parsed from git svn info.
865
866 Returns None if it is not set.
867 """
868 # URL is dependent on the current directory.
869 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
870 if data:
871 keys = dict(line.split(': ', 1) for line in data.splitlines()
872 if ': ' in line)
873 return keys.get('URL', None)
874 return None
875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 def GetRemoteUrl(self):
877 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
878
879 Returns None if there is no remote.
880 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000881 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000882 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
883
884 # If URL is pointing to a local directory, it is probably a git cache.
885 if os.path.isdir(url):
886 url = RunGit(['config', 'remote.%s.url' % remote],
887 error_ok=True,
888 cwd=url).strip()
889 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890
891 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000892 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000893 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000895 self.issue = int(issue) or None if issue else None
896 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 return self.issue
898
899 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000900 if not self.rietveld_server:
901 # If we're on a branch then get the server potentially associated
902 # with that branch.
903 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000904 rietveld_server_config = self._RietveldServer()
905 if rietveld_server_config:
906 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
907 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000908 if not self.rietveld_server:
909 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 return self.rietveld_server
911
912 def GetIssueURL(self):
913 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000914 if not self.GetIssue():
915 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
917
918 def GetDescription(self, pretty=False):
919 if not self.has_description:
920 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000921 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000922 try:
923 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000924 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000925 if e.code == 404:
926 DieWithError(
927 ('\nWhile fetching the description for issue %d, received a '
928 '404 (not found)\n'
929 'error. It is likely that you deleted this '
930 'issue on the server. If this is the\n'
931 'case, please run\n\n'
932 ' git cl issue 0\n\n'
933 'to clear the association with the deleted issue. Then run '
934 'this command again.') % issue)
935 else:
936 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000937 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000938 except urllib2.URLError as e:
939 print >> sys.stderr, (
940 'Warning: Failed to retrieve CL description due to network '
941 'failure.')
942 self.description = ''
943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 self.has_description = True
945 if pretty:
946 wrapper = textwrap.TextWrapper()
947 wrapper.initial_indent = wrapper.subsequent_indent = ' '
948 return wrapper.fill(self.description)
949 return self.description
950
951 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000952 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000953 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 patchset = RunGit(['config', self._PatchsetSetting()],
955 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.patchset = int(patchset) or None if patchset else None
957 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 return self.patchset
959
960 def SetPatchset(self, patchset):
961 """Set this branch's patchset. If patchset=0, clears the patchset."""
962 if patchset:
963 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000964 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965 else:
966 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000967 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000970 def GetMostRecentPatchset(self):
971 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000972
973 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000974 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000975 '/download/issue%s_%s.diff' % (issue, patchset))
976
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000977 def GetIssueProperties(self):
978 if self._props is None:
979 issue = self.GetIssue()
980 if not issue:
981 self._props = {}
982 else:
983 self._props = self.RpcServer().get_issue_properties(issue, True)
984 return self._props
985
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000986 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000987 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000988
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000989 def AddComment(self, message):
990 return self.RpcServer().add_comment(self.GetIssue(), message)
991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992 def SetIssue(self, issue):
993 """Set this branch's issue. If issue=0, clears the issue."""
994 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000995 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 RunGit(['config', self._IssueSetting(), str(issue)])
997 if self.rietveld_server:
998 RunGit(['config', self._RietveldServer(), self.rietveld_server])
999 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001000 current_issue = self.GetIssue()
1001 if current_issue:
1002 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001003 self.issue = None
1004 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001006 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001007 if not self.GitSanityChecks(upstream_branch):
1008 DieWithError('\nGit sanity check failure')
1009
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001010 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001011 if not root:
1012 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001013 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001014
1015 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001016 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001017 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001018 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001019 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001020 except subprocess2.CalledProcessError:
1021 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001022 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001023 'This branch probably doesn\'t exist anymore. To reset the\n'
1024 'tracking branch, please run\n'
1025 ' git branch --set-upstream %s trunk\n'
1026 'replacing trunk with origin/master or the relevant branch') %
1027 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001028
maruel@chromium.org52424302012-08-29 15:14:30 +00001029 issue = self.GetIssue()
1030 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001031 if issue:
1032 description = self.GetDescription()
1033 else:
1034 # If the change was never uploaded, use the log messages of all commits
1035 # up to the branch point, as git cl upload will prefill the description
1036 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001037 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1038 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001039
1040 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001041 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001042 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001043 name,
1044 description,
1045 absroot,
1046 files,
1047 issue,
1048 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001049 author,
1050 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001051
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001052 def GetStatus(self):
1053 """Apply a rough heuristic to give a simple summary of an issue's review
1054 or CQ status, assuming adherence to a common workflow.
1055
1056 Returns None if no issue for this branch, or one of the following keywords:
1057 * 'error' - error from review tool (including deleted issues)
1058 * 'unsent' - not sent for review
1059 * 'waiting' - waiting for review
1060 * 'reply' - waiting for owner to reply to review
1061 * 'lgtm' - LGTM from at least one approved reviewer
1062 * 'commit' - in the commit queue
1063 * 'closed' - closed
1064 """
1065 if not self.GetIssue():
1066 return None
1067
1068 try:
1069 props = self.GetIssueProperties()
1070 except urllib2.HTTPError:
1071 return 'error'
1072
1073 if props.get('closed'):
1074 # Issue is closed.
1075 return 'closed'
1076 if props.get('commit'):
1077 # Issue is in the commit queue.
1078 return 'commit'
1079
1080 try:
1081 reviewers = self.GetApprovingReviewers()
1082 except urllib2.HTTPError:
1083 return 'error'
1084
1085 if reviewers:
1086 # Was LGTM'ed.
1087 return 'lgtm'
1088
1089 messages = props.get('messages') or []
1090
1091 if not messages:
1092 # No message was sent.
1093 return 'unsent'
1094 if messages[-1]['sender'] != props.get('owner_email'):
1095 # Non-LGTM reply from non-owner
1096 return 'reply'
1097 return 'waiting'
1098
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001099 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001100 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001101
1102 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001103 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001104 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001105 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001106 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001107 except presubmit_support.PresubmitFailure, e:
1108 DieWithError(
1109 ('%s\nMaybe your depot_tools is out of date?\n'
1110 'If all fails, contact maruel@') % e)
1111
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001112 def UpdateDescription(self, description):
1113 self.description = description
1114 return self.RpcServer().update_description(
1115 self.GetIssue(), self.description)
1116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001118 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001119 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001121 def SetFlag(self, flag, value):
1122 """Patchset must match."""
1123 if not self.GetPatchset():
1124 DieWithError('The patchset needs to match. Send another patchset.')
1125 try:
1126 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001127 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001128 except urllib2.HTTPError, e:
1129 if e.code == 404:
1130 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1131 if e.code == 403:
1132 DieWithError(
1133 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1134 'match?') % (self.GetIssue(), self.GetPatchset()))
1135 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001137 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 """Returns an upload.RpcServer() to access this review's rietveld instance.
1139 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001140 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001141 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001142 self.GetRietveldServer(),
1143 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001144 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 def _IssueSetting(self):
1147 """Return the git setting that stores this change's issue."""
1148 return 'branch.%s.rietveldissue' % self.GetBranch()
1149
1150 def _PatchsetSetting(self):
1151 """Return the git setting that stores this change's most recent patchset."""
1152 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1153
1154 def _RietveldServer(self):
1155 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001156 branch = self.GetBranch()
1157 if branch:
1158 return 'branch.%s.rietveldserver' % branch
1159 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160
1161
1162def GetCodereviewSettingsInteractively():
1163 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001164 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 server = settings.GetDefaultServerUrl(error_ok=True)
1166 prompt = 'Rietveld server (host[:port])'
1167 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001168 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if not server and not newserver:
1170 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001171 if newserver:
1172 newserver = gclient_utils.UpgradeToHttps(newserver)
1173 if newserver != server:
1174 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001176 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 prompt = caption
1178 if initial:
1179 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001180 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 if new_val == 'x':
1182 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001183 elif new_val:
1184 if is_url:
1185 new_val = gclient_utils.UpgradeToHttps(new_val)
1186 if new_val != initial:
1187 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001189 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001190 SetProperty(settings.GetDefaultPrivateFlag(),
1191 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001193 'tree-status-url', False)
1194 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001195 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001196 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1197 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
1199 # TODO: configure a default branch to diff against, rather than this
1200 # svn-based hackery.
1201
1202
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001203class ChangeDescription(object):
1204 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001205 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001206 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001207
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001208 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001209 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001210
agable@chromium.org42c20792013-09-12 17:34:49 +00001211 @property # www.logilab.org/ticket/89786
1212 def description(self): # pylint: disable=E0202
1213 return '\n'.join(self._description_lines)
1214
1215 def set_description(self, desc):
1216 if isinstance(desc, basestring):
1217 lines = desc.splitlines()
1218 else:
1219 lines = [line.rstrip() for line in desc]
1220 while lines and not lines[0]:
1221 lines.pop(0)
1222 while lines and not lines[-1]:
1223 lines.pop(-1)
1224 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001225
piman@chromium.org336f9122014-09-04 02:16:55 +00001226 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001227 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001228 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001229 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001230 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001231 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001232
agable@chromium.org42c20792013-09-12 17:34:49 +00001233 # Get the set of R= and TBR= lines and remove them from the desciption.
1234 regexp = re.compile(self.R_LINE)
1235 matches = [regexp.match(line) for line in self._description_lines]
1236 new_desc = [l for i, l in enumerate(self._description_lines)
1237 if not matches[i]]
1238 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001239
agable@chromium.org42c20792013-09-12 17:34:49 +00001240 # Construct new unified R= and TBR= lines.
1241 r_names = []
1242 tbr_names = []
1243 for match in matches:
1244 if not match:
1245 continue
1246 people = cleanup_list([match.group(2).strip()])
1247 if match.group(1) == 'TBR':
1248 tbr_names.extend(people)
1249 else:
1250 r_names.extend(people)
1251 for name in r_names:
1252 if name not in reviewers:
1253 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001254 if add_owners_tbr:
1255 owners_db = owners.Database(change.RepositoryRoot(),
1256 fopen=file, os_path=os.path, glob=glob.glob)
1257 all_reviewers = set(tbr_names + reviewers)
1258 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1259 all_reviewers)
1260 tbr_names.extend(owners_db.reviewers_for(missing_files,
1261 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001262 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1263 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1264
1265 # Put the new lines in the description where the old first R= line was.
1266 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1267 if 0 <= line_loc < len(self._description_lines):
1268 if new_tbr_line:
1269 self._description_lines.insert(line_loc, new_tbr_line)
1270 if new_r_line:
1271 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001272 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001273 if new_r_line:
1274 self.append_footer(new_r_line)
1275 if new_tbr_line:
1276 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001277
1278 def prompt(self):
1279 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001280 self.set_description([
1281 '# Enter a description of the change.',
1282 '# This will be displayed on the codereview site.',
1283 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001284 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001285 '--------------------',
1286 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001287
agable@chromium.org42c20792013-09-12 17:34:49 +00001288 regexp = re.compile(self.BUG_LINE)
1289 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001290 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001291 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001292 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001293 if not content:
1294 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001295 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001296
1297 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001298 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1299 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001300 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001301 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001302
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001303 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001304 if self._description_lines:
1305 # Add an empty line if either the last line or the new line isn't a tag.
1306 last_line = self._description_lines[-1]
1307 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1308 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1309 self._description_lines.append('')
1310 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001311
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001312 def get_reviewers(self):
1313 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001314 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1315 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001316 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001317
1318
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001319def get_approving_reviewers(props):
1320 """Retrieves the reviewers that approved a CL from the issue properties with
1321 messages.
1322
1323 Note that the list may contain reviewers that are not committer, thus are not
1324 considered by the CQ.
1325 """
1326 return sorted(
1327 set(
1328 message['sender']
1329 for message in props['messages']
1330 if message['approval'] and message['sender'] in props['reviewers']
1331 )
1332 )
1333
1334
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335def FindCodereviewSettingsFile(filename='codereview.settings'):
1336 """Finds the given file starting in the cwd and going up.
1337
1338 Only looks up to the top of the repository unless an
1339 'inherit-review-settings-ok' file exists in the root of the repository.
1340 """
1341 inherit_ok_file = 'inherit-review-settings-ok'
1342 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001343 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1345 root = '/'
1346 while True:
1347 if filename in os.listdir(cwd):
1348 if os.path.isfile(os.path.join(cwd, filename)):
1349 return open(os.path.join(cwd, filename))
1350 if cwd == root:
1351 break
1352 cwd = os.path.dirname(cwd)
1353
1354
1355def LoadCodereviewSettingsFromFile(fileobj):
1356 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001357 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 def SetProperty(name, setting, unset_error_ok=False):
1360 fullname = 'rietveld.' + name
1361 if setting in keyvals:
1362 RunGit(['config', fullname, keyvals[setting]])
1363 else:
1364 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1365
1366 SetProperty('server', 'CODE_REVIEW_SERVER')
1367 # Only server setting is required. Other settings can be absent.
1368 # In that case, we ignore errors raised during option deletion attempt.
1369 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001370 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1372 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001373 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001374 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001375 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1376 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001377 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001378 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001379 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001380 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1381 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001383 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001384 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1387 #should be of the form
1388 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1389 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1390 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1391 keyvals['ORIGIN_URL_CONFIG']])
1392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001394def urlretrieve(source, destination):
1395 """urllib is broken for SSL connections via a proxy therefore we
1396 can't use urllib.urlretrieve()."""
1397 with open(destination, 'w') as f:
1398 f.write(urllib2.urlopen(source).read())
1399
1400
ukai@chromium.org712d6102013-11-27 00:52:58 +00001401def hasSheBang(fname):
1402 """Checks fname is a #! script."""
1403 with open(fname) as f:
1404 return f.read(2).startswith('#!')
1405
1406
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001407def DownloadHooks(force):
1408 """downloads hooks
1409
1410 Args:
1411 force: True to update hooks. False to install hooks if not present.
1412 """
1413 if not settings.GetIsGerrit():
1414 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001415 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001416 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1417 if not os.access(dst, os.X_OK):
1418 if os.path.exists(dst):
1419 if not force:
1420 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001421 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001422 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001423 if not hasSheBang(dst):
1424 DieWithError('Not a script: %s\n'
1425 'You need to download from\n%s\n'
1426 'into .git/hooks/commit-msg and '
1427 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001428 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1429 except Exception:
1430 if os.path.exists(dst):
1431 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001432 DieWithError('\nFailed to download hooks.\n'
1433 'You need to download from\n%s\n'
1434 'into .git/hooks/commit-msg and '
1435 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001436
1437
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001438@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001440 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001442 parser.add_option('--activate-update', action='store_true',
1443 help='activate auto-updating [rietveld] section in '
1444 '.git/config')
1445 parser.add_option('--deactivate-update', action='store_true',
1446 help='deactivate auto-updating [rietveld] section in '
1447 '.git/config')
1448 options, args = parser.parse_args(args)
1449
1450 if options.deactivate_update:
1451 RunGit(['config', 'rietveld.autoupdate', 'false'])
1452 return
1453
1454 if options.activate_update:
1455 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1456 return
1457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 if len(args) == 0:
1459 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001460 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 return 0
1462
1463 url = args[0]
1464 if not url.endswith('codereview.settings'):
1465 url = os.path.join(url, 'codereview.settings')
1466
1467 # Load code review settings and download hooks (if available).
1468 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001469 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470 return 0
1471
1472
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001473def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001474 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001475 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1476 branch = ShortBranchName(branchref)
1477 _, args = parser.parse_args(args)
1478 if not args:
1479 print("Current base-url:")
1480 return RunGit(['config', 'branch.%s.base-url' % branch],
1481 error_ok=False).strip()
1482 else:
1483 print("Setting base-url to %s" % args[0])
1484 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1485 error_ok=False).strip()
1486
1487
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001488def color_for_status(status):
1489 """Maps a Changelist status to color, for CMDstatus and other tools."""
1490 return {
1491 'unsent': Fore.RED,
1492 'waiting': Fore.BLUE,
1493 'reply': Fore.YELLOW,
1494 'lgtm': Fore.GREEN,
1495 'commit': Fore.MAGENTA,
1496 'closed': Fore.CYAN,
1497 'error': Fore.WHITE,
1498 }.get(status, Fore.WHITE)
1499
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001500def fetch_cl_status(branch, auth_config=None):
1501 """Fetches information for an issue and returns (branch, issue, status)."""
1502 cl = Changelist(branchref=branch, auth_config=auth_config)
1503 url = cl.GetIssueURL()
1504 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001505
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001506 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001507 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001508 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001509
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001510 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001511
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001512def get_cl_statuses(
1513 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001514 """Returns a blocking iterable of (branch, issue, color) for given branches.
1515
1516 If fine_grained is true, this will fetch CL statuses from the server.
1517 Otherwise, simply indicate if there's a matching url for the given branches.
1518
1519 If max_processes is specified, it is used as the maximum number of processes
1520 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1521 spawned.
1522 """
1523 # Silence upload.py otherwise it becomes unwieldly.
1524 upload.verbosity = 0
1525
1526 if fine_grained:
1527 # Process one branch synchronously to work through authentication, then
1528 # spawn processes to process all the other branches in parallel.
1529 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001530 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1531 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001532
1533 branches_to_fetch = branches[1:]
1534 pool = ThreadPool(
1535 min(max_processes, len(branches_to_fetch))
1536 if max_processes is not None
1537 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001538 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001539 yield x
1540 else:
1541 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1542 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001543 cl = Changelist(branchref=b, auth_config=auth_config)
1544 url = cl.GetIssueURL()
1545 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001546
rmistry@google.com2dd99862015-06-22 12:22:18 +00001547
1548def upload_branch_deps(cl, args):
1549 """Uploads CLs of local branches that are dependents of the current branch.
1550
1551 If the local branch dependency tree looks like:
1552 test1 -> test2.1 -> test3.1
1553 -> test3.2
1554 -> test2.2 -> test3.3
1555
1556 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1557 run on the dependent branches in this order:
1558 test2.1, test3.1, test3.2, test2.2, test3.3
1559
1560 Note: This function does not rebase your local dependent branches. Use it when
1561 you make a change to the parent branch that will not conflict with its
1562 dependent branches, and you would like their dependencies updated in
1563 Rietveld.
1564 """
1565 if git_common.is_dirty_git_tree('upload-branch-deps'):
1566 return 1
1567
1568 root_branch = cl.GetBranch()
1569 if root_branch is None:
1570 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1571 'Get on a branch!')
1572 if not cl.GetIssue() or not cl.GetPatchset():
1573 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1574 'patchset dependencies without an uploaded CL.')
1575
1576 branches = RunGit(['for-each-ref',
1577 '--format=%(refname:short) %(upstream:short)',
1578 'refs/heads'])
1579 if not branches:
1580 print('No local branches found.')
1581 return 0
1582
1583 # Create a dictionary of all local branches to the branches that are dependent
1584 # on it.
1585 tracked_to_dependents = collections.defaultdict(list)
1586 for b in branches.splitlines():
1587 tokens = b.split()
1588 if len(tokens) == 2:
1589 branch_name, tracked = tokens
1590 tracked_to_dependents[tracked].append(branch_name)
1591
1592 print
1593 print 'The dependent local branches of %s are:' % root_branch
1594 dependents = []
1595 def traverse_dependents_preorder(branch, padding=''):
1596 dependents_to_process = tracked_to_dependents.get(branch, [])
1597 padding += ' '
1598 for dependent in dependents_to_process:
1599 print '%s%s' % (padding, dependent)
1600 dependents.append(dependent)
1601 traverse_dependents_preorder(dependent, padding)
1602 traverse_dependents_preorder(root_branch)
1603 print
1604
1605 if not dependents:
1606 print 'There are no dependent local branches for %s' % root_branch
1607 return 0
1608
1609 print ('This command will checkout all dependent branches and run '
1610 '"git cl upload".')
1611 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1612
1613 # Add a default patchset title to all upload calls.
1614 args.extend(['-t', 'Updated patchset dependency'])
1615 # Record all dependents that failed to upload.
1616 failures = {}
1617 # Go through all dependents, checkout the branch and upload.
1618 try:
1619 for dependent_branch in dependents:
1620 print
1621 print '--------------------------------------'
1622 print 'Running "git cl upload" from %s:' % dependent_branch
1623 RunGit(['checkout', '-q', dependent_branch])
1624 print
1625 try:
1626 if CMDupload(OptionParser(), args) != 0:
1627 print 'Upload failed for %s!' % dependent_branch
1628 failures[dependent_branch] = 1
1629 except: # pylint: disable=W0702
1630 failures[dependent_branch] = 1
1631 print
1632 finally:
1633 # Swap back to the original root branch.
1634 RunGit(['checkout', '-q', root_branch])
1635
1636 print
1637 print 'Upload complete for dependent branches!'
1638 for dependent_branch in dependents:
1639 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1640 print ' %s : %s' % (dependent_branch, upload_status)
1641 print
1642
1643 return 0
1644
1645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001647 """Show status of changelists.
1648
1649 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001650 - Red not sent for review or broken
1651 - Blue waiting for review
1652 - Yellow waiting for you to reply to review
1653 - Green LGTM'ed
1654 - Magenta in the commit queue
1655 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001656
1657 Also see 'git cl comments'.
1658 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001659 parser.add_option('--field',
1660 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001661 parser.add_option('-f', '--fast', action='store_true',
1662 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001663 parser.add_option(
1664 '-j', '--maxjobs', action='store', type=int,
1665 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001666
1667 auth.add_auth_options(parser)
1668 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001669 if args:
1670 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001671 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 if options.field.startswith('desc'):
1676 print cl.GetDescription()
1677 elif options.field == 'id':
1678 issueid = cl.GetIssue()
1679 if issueid:
1680 print issueid
1681 elif options.field == 'patch':
1682 patchset = cl.GetPatchset()
1683 if patchset:
1684 print patchset
1685 elif options.field == 'url':
1686 url = cl.GetIssueURL()
1687 if url:
1688 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001689 return 0
1690
1691 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1692 if not branches:
1693 print('No local branch found.')
1694 return 0
1695
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001696 changes = (
1697 Changelist(branchref=b, auth_config=auth_config)
1698 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001699 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001700 alignment = max(5, max(len(b) for b in branches))
1701 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001702 output = get_cl_statuses(branches,
1703 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001704 max_processes=options.maxjobs,
1705 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001706
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001707 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001708 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001709 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001710 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001711 b, i, status = output.next()
1712 branch_statuses[b] = (i, status)
1713 issue_url, status = branch_statuses.pop(branch)
1714 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001715 reset = Fore.RESET
1716 if not sys.stdout.isatty():
1717 color = ''
1718 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001719 status_str = '(%s)' % status if status else ''
1720 print ' %*s : %s%s %s%s' % (
1721 alignment, ShortBranchName(branch), color, issue_url, status_str,
1722 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001723
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001724 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001725 print
1726 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001727 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001728 if not cl.GetIssue():
1729 print 'No issue assigned.'
1730 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001731 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001732 if not options.fast:
1733 print 'Issue description:'
1734 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 return 0
1736
1737
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001738def colorize_CMDstatus_doc():
1739 """To be called once in main() to add colors to git cl status help."""
1740 colors = [i for i in dir(Fore) if i[0].isupper()]
1741
1742 def colorize_line(line):
1743 for color in colors:
1744 if color in line.upper():
1745 # Extract whitespaces first and the leading '-'.
1746 indent = len(line) - len(line.lstrip(' ')) + 1
1747 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1748 return line
1749
1750 lines = CMDstatus.__doc__.splitlines()
1751 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1752
1753
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001754@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001756 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757
1758 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001759 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001760 parser.add_option('-r', '--reverse', action='store_true',
1761 help='Lookup the branch(es) for the specified issues. If '
1762 'no issues are specified, all branches with mapped '
1763 'issues will be listed.')
1764 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
dnj@chromium.org406c4402015-03-03 17:22:28 +00001766 if options.reverse:
1767 branches = RunGit(['for-each-ref', 'refs/heads',
1768 '--format=%(refname:short)']).splitlines()
1769
1770 # Reverse issue lookup.
1771 issue_branch_map = {}
1772 for branch in branches:
1773 cl = Changelist(branchref=branch)
1774 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1775 if not args:
1776 args = sorted(issue_branch_map.iterkeys())
1777 for issue in args:
1778 if not issue:
1779 continue
1780 print 'Branch for issue number %s: %s' % (
1781 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1782 else:
1783 cl = Changelist()
1784 if len(args) > 0:
1785 try:
1786 issue = int(args[0])
1787 except ValueError:
1788 DieWithError('Pass a number to set the issue or none to list it.\n'
1789 'Maybe you want to run git cl status?')
1790 cl.SetIssue(issue)
1791 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 return 0
1793
1794
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001795def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001796 """Shows or posts review comments for any changelist."""
1797 parser.add_option('-a', '--add-comment', dest='comment',
1798 help='comment to add to an issue')
1799 parser.add_option('-i', dest='issue',
1800 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001801 parser.add_option('-j', '--json-file',
1802 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001803 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001804 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001805 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001806
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001807 issue = None
1808 if options.issue:
1809 try:
1810 issue = int(options.issue)
1811 except ValueError:
1812 DieWithError('A review issue id is expected to be a number')
1813
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001814 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001815
1816 if options.comment:
1817 cl.AddComment(options.comment)
1818 return 0
1819
1820 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001821 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001822 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001823 summary.append({
1824 'date': message['date'],
1825 'lgtm': False,
1826 'message': message['text'],
1827 'not_lgtm': False,
1828 'sender': message['sender'],
1829 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001830 if message['disapproval']:
1831 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001832 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001833 elif message['approval']:
1834 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001835 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001836 elif message['sender'] == data['owner_email']:
1837 color = Fore.MAGENTA
1838 else:
1839 color = Fore.BLUE
1840 print '\n%s%s %s%s' % (
1841 color, message['date'].split('.', 1)[0], message['sender'],
1842 Fore.RESET)
1843 if message['text'].strip():
1844 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001845 if options.json_file:
1846 with open(options.json_file, 'wb') as f:
1847 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001848 return 0
1849
1850
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001851def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001852 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001853 parser.add_option('-d', '--display', action='store_true',
1854 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001855 auth.add_auth_options(parser)
1856 options, _ = parser.parse_args(args)
1857 auth_config = auth.extract_auth_config_from_options(options)
1858 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001859 if not cl.GetIssue():
1860 DieWithError('This branch has no associated changelist.')
1861 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001862 if options.display:
1863 print description.description
1864 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001865 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001866 if cl.GetDescription() != description.description:
1867 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001868 return 0
1869
1870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871def CreateDescriptionFromLog(args):
1872 """Pulls out the commit log to use as a base for the CL description."""
1873 log_args = []
1874 if len(args) == 1 and not args[0].endswith('.'):
1875 log_args = [args[0] + '..']
1876 elif len(args) == 1 and args[0].endswith('...'):
1877 log_args = [args[0][:-1]]
1878 elif len(args) == 2:
1879 log_args = [args[0] + '..' + args[1]]
1880 else:
1881 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001882 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883
1884
thestig@chromium.org44202a22014-03-11 19:22:18 +00001885def CMDlint(parser, args):
1886 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001887 parser.add_option('--filter', action='append', metavar='-x,+y',
1888 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001889 auth.add_auth_options(parser)
1890 options, args = parser.parse_args(args)
1891 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001892
1893 # Access to a protected member _XX of a client class
1894 # pylint: disable=W0212
1895 try:
1896 import cpplint
1897 import cpplint_chromium
1898 except ImportError:
1899 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1900 return 1
1901
1902 # Change the current working directory before calling lint so that it
1903 # shows the correct base.
1904 previous_cwd = os.getcwd()
1905 os.chdir(settings.GetRoot())
1906 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001907 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001908 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1909 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001910 if not files:
1911 print "Cannot lint an empty CL"
1912 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001913
1914 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001915 command = args + files
1916 if options.filter:
1917 command = ['--filter=' + ','.join(options.filter)] + command
1918 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001919
1920 white_regex = re.compile(settings.GetLintRegex())
1921 black_regex = re.compile(settings.GetLintIgnoreRegex())
1922 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1923 for filename in filenames:
1924 if white_regex.match(filename):
1925 if black_regex.match(filename):
1926 print "Ignoring file %s" % filename
1927 else:
1928 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1929 extra_check_functions)
1930 else:
1931 print "Skipping file %s" % filename
1932 finally:
1933 os.chdir(previous_cwd)
1934 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1935 if cpplint._cpplint_state.error_count != 0:
1936 return 1
1937 return 0
1938
1939
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001941 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001942 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001944 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001945 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001946 auth.add_auth_options(parser)
1947 options, args = parser.parse_args(args)
1948 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001949
sbc@chromium.org71437c02015-04-09 19:29:40 +00001950 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001951 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952 return 1
1953
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001954 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955 if args:
1956 base_branch = args[0]
1957 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001958 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001959 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001961 cl.RunHook(
1962 committing=not options.upload,
1963 may_prompt=False,
1964 verbose=options.verbose,
1965 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001966 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967
1968
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001969def AddChangeIdToCommitMessage(options, args):
1970 """Re-commits using the current message, assumes the commit hook is in
1971 place.
1972 """
1973 log_desc = options.message or CreateDescriptionFromLog(args)
1974 git_command = ['commit', '--amend', '-m', log_desc]
1975 RunGit(git_command)
1976 new_log_desc = CreateDescriptionFromLog(args)
1977 if CHANGE_ID in new_log_desc:
1978 print 'git-cl: Added Change-Id to commit message.'
1979 else:
1980 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1981
1982
piman@chromium.org336f9122014-09-04 02:16:55 +00001983def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001984 """upload the current branch to gerrit."""
1985 # We assume the remote called "origin" is the one we want.
1986 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001987 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001988
1989 remote, remote_branch = cl.GetRemoteBranch()
1990 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1991 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001993 change_desc = ChangeDescription(
1994 options.message or CreateDescriptionFromLog(args))
1995 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001996 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001998
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001999 if options.squash:
2000 # Try to get the message from a previous upload.
2001 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
2002 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
2003 if not message:
2004 if not options.force:
2005 change_desc.prompt()
2006
2007 if CHANGE_ID not in change_desc.description:
2008 # Run the commit-msg hook without modifying the head commit by writing
2009 # the commit message to a temporary file and running the hook over it,
2010 # then reading the file back in.
2011 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2012 'commit-msg')
2013 file_handle, msg_file = tempfile.mkstemp(text=True,
2014 prefix='commit_msg')
2015 try:
2016 try:
2017 with os.fdopen(file_handle, 'w') as fileobj:
2018 fileobj.write(change_desc.description)
2019 finally:
2020 os.close(file_handle)
2021 RunCommand([commit_msg_hook, msg_file])
2022 change_desc.set_description(gclient_utils.FileRead(msg_file))
2023 finally:
2024 os.remove(msg_file)
2025
2026 if not change_desc.description:
2027 print "Description is empty; aborting."
2028 return 1
2029
2030 message = change_desc.description
2031
2032 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2033 if remote is '.':
2034 # If our upstream branch is local, we base our squashed commit on its
2035 # squashed version.
2036 parent = ('refs/heads/git_cl_uploads/' +
2037 scm.GIT.ShortBranchName(upstream_branch))
2038
2039 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2040 # will create additional CLs when uploading.
2041 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2042 RunGitSilent(['rev-parse', parent + ':'])):
2043 print 'Upload upstream branch ' + upstream_branch + ' first.'
2044 return 1
2045 else:
2046 parent = cl.GetCommonAncestorWithUpstream()
2047
2048 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2049 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2050 '-m', message]).strip()
2051 else:
2052 if CHANGE_ID not in change_desc.description:
2053 AddChangeIdToCommitMessage(options, args)
2054 ref_to_push = 'HEAD'
2055 parent = '%s/%s' % (gerrit_remote, branch)
2056
2057 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2058 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002059 if len(commits) > 1:
2060 print('WARNING: This will upload %d commits. Run the following command '
2061 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002062 print('git log %s..%s' % (parent, ref_to_push))
2063 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002064 'commit.')
2065 ask_for_data('About to upload; enter to confirm.')
2066
piman@chromium.org336f9122014-09-04 02:16:55 +00002067 if options.reviewers or options.tbr_owners:
2068 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069
ukai@chromium.orge8077812012-02-03 03:41:46 +00002070 receive_options = []
2071 cc = cl.GetCCList().split(',')
2072 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002073 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002074 cc = filter(None, cc)
2075 if cc:
2076 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002077 if change_desc.get_reviewers():
2078 receive_options.extend(
2079 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080
ukai@chromium.orge8077812012-02-03 03:41:46 +00002081 git_command = ['push']
2082 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002083 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002084 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002085 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002086 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002087
2088 if options.squash:
2089 head = RunGit(['rev-parse', 'HEAD']).strip()
2090 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2091
ukai@chromium.orge8077812012-02-03 03:41:46 +00002092 # TODO(ukai): parse Change-Id: and set issue number?
2093 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095
wittman@chromium.org455dc922015-01-26 20:15:50 +00002096def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2097 """Computes the remote branch ref to use for the CL.
2098
2099 Args:
2100 remote (str): The git remote for the CL.
2101 remote_branch (str): The git remote branch for the CL.
2102 target_branch (str): The target branch specified by the user.
2103 pending_prefix (str): The pending prefix from the settings.
2104 """
2105 if not (remote and remote_branch):
2106 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002107
wittman@chromium.org455dc922015-01-26 20:15:50 +00002108 if target_branch:
2109 # Cannonicalize branch references to the equivalent local full symbolic
2110 # refs, which are then translated into the remote full symbolic refs
2111 # below.
2112 if '/' not in target_branch:
2113 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2114 else:
2115 prefix_replacements = (
2116 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2117 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2118 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2119 )
2120 match = None
2121 for regex, replacement in prefix_replacements:
2122 match = re.search(regex, target_branch)
2123 if match:
2124 remote_branch = target_branch.replace(match.group(0), replacement)
2125 break
2126 if not match:
2127 # This is a branch path but not one we recognize; use as-is.
2128 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002129 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2130 # Handle the refs that need to land in different refs.
2131 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002132
wittman@chromium.org455dc922015-01-26 20:15:50 +00002133 # Create the true path to the remote branch.
2134 # Does the following translation:
2135 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2136 # * refs/remotes/origin/master -> refs/heads/master
2137 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2138 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2139 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2140 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2141 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2142 'refs/heads/')
2143 elif remote_branch.startswith('refs/remotes/branch-heads'):
2144 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2145 # If a pending prefix exists then replace refs/ with it.
2146 if pending_prefix:
2147 remote_branch = remote_branch.replace('refs/', pending_prefix)
2148 return remote_branch
2149
2150
piman@chromium.org336f9122014-09-04 02:16:55 +00002151def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002152 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002153 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2154 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002155 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 if options.emulate_svn_auto_props:
2157 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158
2159 change_desc = None
2160
pgervais@chromium.org91141372014-01-09 23:27:20 +00002161 if options.email is not None:
2162 upload_args.extend(['--email', options.email])
2163
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002164 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002165 if options.title:
2166 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002167 if options.message:
2168 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002169 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 print ("This branch is associated with issue %s. "
2171 "Adding patch to that issue." % cl.GetIssue())
2172 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002173 if options.title:
2174 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002175 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002176 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002177 if options.reviewers or options.tbr_owners:
2178 change_desc.update_reviewers(options.reviewers,
2179 options.tbr_owners,
2180 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002181 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002182 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002183
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002184 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002185 print "Description is empty; aborting."
2186 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002187
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002188 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002189 if change_desc.get_reviewers():
2190 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002191 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002192 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002193 DieWithError("Must specify reviewers to send email.")
2194 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002195
2196 # We check this before applying rietveld.private assuming that in
2197 # rietveld.cc only addresses which we can send private CLs to are listed
2198 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2199 # --private is specified explicitly on the command line.
2200 if options.private:
2201 logging.warn('rietveld.cc is ignored since private flag is specified. '
2202 'You need to review and add them manually if necessary.')
2203 cc = cl.GetCCListWithoutDefault()
2204 else:
2205 cc = cl.GetCCList()
2206 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002207 if cc:
2208 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002210 if options.private or settings.GetDefaultPrivateFlag() == "True":
2211 upload_args.append('--private')
2212
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002213 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002214 if not options.find_copies:
2215 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217 # Include the upstream repo's URL in the change -- this is useful for
2218 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002219 remote_url = cl.GetGitBaseUrlFromConfig()
2220 if not remote_url:
2221 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002222 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002223 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002224 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2225 remote_url = (cl.GetRemoteUrl() + '@'
2226 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227 if remote_url:
2228 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002229 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002230 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2231 settings.GetPendingRefPrefix())
2232 if target_ref:
2233 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002235 # Look for dependent patchsets. See crbug.com/480453 for more details.
2236 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2237 upstream_branch = ShortBranchName(upstream_branch)
2238 if remote is '.':
2239 # A local branch is being tracked.
2240 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002241 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002242 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002243 print ('Skipping dependency patchset upload because git config '
2244 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002245 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002246 else:
2247 auth_config = auth.extract_auth_config_from_options(options)
2248 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2249 branch_cl_issue_url = branch_cl.GetIssueURL()
2250 branch_cl_issue = branch_cl.GetIssue()
2251 branch_cl_patchset = branch_cl.GetPatchset()
2252 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2253 upload_args.extend(
2254 ['--depends_on_patchset', '%s:%s' % (
2255 branch_cl_issue, branch_cl_patchset)])
2256 print
2257 print ('The current branch (%s) is tracking a local branch (%s) with '
2258 'an associated CL.') % (cl.GetBranch(), local_branch)
2259 print 'Adding %s/#ps%s as a dependency patchset.' % (
2260 branch_cl_issue_url, branch_cl_patchset)
2261 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002262
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002263 project = settings.GetProject()
2264 if project:
2265 upload_args.extend(['--project', project])
2266
rmistry@google.comef966222015-04-07 11:15:01 +00002267 if options.cq_dry_run:
2268 upload_args.extend(['--cq_dry_run'])
2269
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002270 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002271 upload_args = ['upload'] + upload_args + args
2272 logging.info('upload.RealMain(%s)', upload_args)
2273 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002274 issue = int(issue)
2275 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002276 except KeyboardInterrupt:
2277 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278 except:
2279 # If we got an exception after the user typed a description for their
2280 # change, back up the description before re-raising.
2281 if change_desc:
2282 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2283 print '\nGot exception while uploading -- saving description to %s\n' \
2284 % backup_path
2285 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002286 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287 backup_file.close()
2288 raise
2289
2290 if not cl.GetIssue():
2291 cl.SetIssue(issue)
2292 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002293
2294 if options.use_commit_queue:
2295 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002296 return 0
2297
2298
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002299def cleanup_list(l):
2300 """Fixes a list so that comma separated items are put as individual items.
2301
2302 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2303 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2304 """
2305 items = sum((i.split(',') for i in l), [])
2306 stripped_items = (i.strip() for i in items)
2307 return sorted(filter(None, stripped_items))
2308
2309
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002310@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002311def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002312 """Uploads the current changelist to codereview.
2313
2314 Can skip dependency patchset uploads for a branch by running:
2315 git config branch.branch_name.skip-deps-uploads True
2316 To unset run:
2317 git config --unset branch.branch_name.skip-deps-uploads
2318 Can also set the above globally by using the --global flag.
2319 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002320 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2321 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002322 parser.add_option('--bypass-watchlists', action='store_true',
2323 dest='bypass_watchlists',
2324 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002325 parser.add_option('-f', action='store_true', dest='force',
2326 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002327 parser.add_option('-m', dest='message', help='message for patchset')
2328 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002329 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002330 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002331 help='reviewer email addresses')
2332 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002333 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002334 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002335 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002336 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002337 parser.add_option('--emulate_svn_auto_props',
2338 '--emulate-svn-auto-props',
2339 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002340 dest="emulate_svn_auto_props",
2341 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002342 parser.add_option('-c', '--use-commit-queue', action='store_true',
2343 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002344 parser.add_option('--private', action='store_true',
2345 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002346 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002347 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002348 metavar='TARGET',
2349 help='Apply CL to remote ref TARGET. ' +
2350 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002351 parser.add_option('--squash', action='store_true',
2352 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002353 parser.add_option('--email', default=None,
2354 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002355 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2356 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002357 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2358 help='Send the patchset to do a CQ dry run right after '
2359 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002360 parser.add_option('--dependencies', action='store_true',
2361 help='Uploads CLs of all the local branches that depend on '
2362 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002363
rmistry@google.com2dd99862015-06-22 12:22:18 +00002364 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002365 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002366 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002367 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002368 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002369
sbc@chromium.org71437c02015-04-09 19:29:40 +00002370 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002371 return 1
2372
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002373 options.reviewers = cleanup_list(options.reviewers)
2374 options.cc = cleanup_list(options.cc)
2375
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002376 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002377 if args:
2378 # TODO(ukai): is it ok for gerrit case?
2379 base_branch = args[0]
2380 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002381 if cl.GetBranch() is None:
2382 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2383
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002384 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002385 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002386 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002387
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002388 # Make sure authenticated to Rietveld before running expensive hooks. It is
2389 # a fast, best efforts check. Rietveld still can reject the authentication
2390 # during the actual upload.
2391 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2392 authenticator = auth.get_authenticator_for_host(
2393 cl.GetRietveldServer(), auth_config)
2394 if not authenticator.has_cached_credentials():
2395 raise auth.LoginRequiredError(cl.GetRietveldServer())
2396
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002397 # Apply watchlists on upload.
2398 change = cl.GetChange(base_branch, None)
2399 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2400 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002401 if not options.bypass_watchlists:
2402 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002403
ukai@chromium.orge8077812012-02-03 03:41:46 +00002404 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002405 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002406 # Set the reviewer list now so that presubmit checks can access it.
2407 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002408 change_description.update_reviewers(options.reviewers,
2409 options.tbr_owners,
2410 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002411 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002412 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002413 may_prompt=not options.force,
2414 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002415 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002416 if not hook_results.should_continue():
2417 return 1
2418 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002419 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002420
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002421 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002422 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002423 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002424 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002425 print ('The last upload made from this repository was patchset #%d but '
2426 'the most recent patchset on the server is #%d.'
2427 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002428 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2429 'from another machine or branch the patch you\'re uploading now '
2430 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002431 ask_for_data('About to upload; enter to confirm.')
2432
iannucci@chromium.org79540052012-10-19 23:15:26 +00002433 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002434 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002435 return GerritUpload(options, args, cl, change)
2436 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002437 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002438 git_set_branch_value('last-upload-hash',
2439 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002440 # Run post upload hooks, if specified.
2441 if settings.GetRunPostUploadHook():
2442 presubmit_support.DoPostUploadExecuter(
2443 change,
2444 cl,
2445 settings.GetRoot(),
2446 options.verbose,
2447 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002448
rmistry@google.com2dd99862015-06-22 12:22:18 +00002449 # Upload all dependencies if specified.
2450 if options.dependencies:
2451 print
2452 print '--dependencies has been specified.'
2453 print 'All dependent local branches will be re-uploaded.'
2454 print
2455 # Remove the dependencies flag from args so that we do not end up in a
2456 # loop.
2457 orig_args.remove('--dependencies')
2458 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002459 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002460
2461
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002462def IsSubmoduleMergeCommit(ref):
2463 # When submodules are added to the repo, we expect there to be a single
2464 # non-git-svn merge commit at remote HEAD with a signature comment.
2465 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002466 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002467 return RunGit(cmd) != ''
2468
2469
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002470def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002471 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002473 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 Updates changelog with metadata (e.g. pointer to review).
2475 Pushes/dcommits the code upstream.
2476 Updates review and closes.
2477 """
2478 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2479 help='bypass upload presubmit hook')
2480 parser.add_option('-m', dest='message',
2481 help="override review description")
2482 parser.add_option('-f', action='store_true', dest='force',
2483 help="force yes to questions (don't prompt)")
2484 parser.add_option('-c', dest='contributor',
2485 help="external contributor for patch (appended to " +
2486 "description and used as author for git). Should be " +
2487 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002488 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002489 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002490 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002491 auth_config = auth.extract_auth_config_from_options(options)
2492
2493 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002495 current = cl.GetBranch()
2496 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2497 if not settings.GetIsGitSvn() and remote == '.':
2498 print
2499 print 'Attempting to push branch %r into another local branch!' % current
2500 print
2501 print 'Either reparent this branch on top of origin/master:'
2502 print ' git reparent-branch --root'
2503 print
2504 print 'OR run `git rebase-update` if you think the parent branch is already'
2505 print 'committed.'
2506 print
2507 print ' Current parent: %r' % upstream_branch
2508 return 1
2509
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002510 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002511 # Default to merging against our best guess of the upstream branch.
2512 args = [cl.GetUpstreamBranch()]
2513
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002514 if options.contributor:
2515 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2516 print "Please provide contibutor as 'First Last <email@example.com>'"
2517 return 1
2518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002519 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002520 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002521
sbc@chromium.org71437c02015-04-09 19:29:40 +00002522 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523 return 1
2524
2525 # This rev-list syntax means "show all commits not in my branch that
2526 # are in base_branch".
2527 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2528 base_branch]).splitlines()
2529 if upstream_commits:
2530 print ('Base branch "%s" has %d commits '
2531 'not in this branch.' % (base_branch, len(upstream_commits)))
2532 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2533 return 1
2534
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002535 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002536 svn_head = None
2537 if cmd == 'dcommit' or base_has_submodules:
2538 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2539 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002541 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002542 # If the base_head is a submodule merge commit, the first parent of the
2543 # base_head should be a git-svn commit, which is what we're interested in.
2544 base_svn_head = base_branch
2545 if base_has_submodules:
2546 base_svn_head += '^1'
2547
2548 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002549 if extra_commits:
2550 print ('This branch has %d additional commits not upstreamed yet.'
2551 % len(extra_commits.splitlines()))
2552 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2553 'before attempting to %s.' % (base_branch, cmd))
2554 return 1
2555
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002556 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002557 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002558 author = None
2559 if options.contributor:
2560 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002561 hook_results = cl.RunHook(
2562 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002563 may_prompt=not options.force,
2564 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002565 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002566 if not hook_results.should_continue():
2567 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002568
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002569 # Check the tree status if the tree status URL is set.
2570 status = GetTreeStatus()
2571 if 'closed' == status:
2572 print('The tree is closed. Please wait for it to reopen. Use '
2573 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2574 return 1
2575 elif 'unknown' == status:
2576 print('Unable to determine tree status. Please verify manually and '
2577 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2578 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002579 else:
2580 breakpad.SendStack(
2581 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002582 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2583 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002584 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586 change_desc = ChangeDescription(options.message)
2587 if not change_desc.description and cl.GetIssue():
2588 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002589
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002590 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002591 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002592 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002593 else:
2594 print 'No description set.'
2595 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2596 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002597
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002598 # Keep a separate copy for the commit message, because the commit message
2599 # contains the link to the Rietveld issue, while the Rietveld message contains
2600 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002601 # Keep a separate copy for the commit message.
2602 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002603 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002604
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002605 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002606 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002607 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002608 # after it. Add a period on a new line to circumvent this. Also add a space
2609 # before the period to make sure that Gitiles continues to correctly resolve
2610 # the URL.
2611 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002612 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002613 commit_desc.append_footer('Patch from %s.' % options.contributor)
2614
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002615 print('Description:')
2616 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002617
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002618 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002619 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002620 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002621
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002622 # We want to squash all this branch's commits into one commit with the proper
2623 # description. We do this by doing a "reset --soft" to the base branch (which
2624 # keeps the working copy the same), then dcommitting that. If origin/master
2625 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2626 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002627 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002628 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2629 # Delete the branches if they exist.
2630 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2631 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2632 result = RunGitWithCode(showref_cmd)
2633 if result[0] == 0:
2634 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002635
2636 # We might be in a directory that's present in this branch but not in the
2637 # trunk. Move up to the top of the tree so that git commands that expect a
2638 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002639 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 if rel_base_path:
2641 os.chdir(rel_base_path)
2642
2643 # Stuff our change into the merge branch.
2644 # We wrap in a try...finally block so if anything goes wrong,
2645 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002646 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002647 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002648 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002649 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002651 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002652 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002653 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002654 RunGit(
2655 [
2656 'commit', '--author', options.contributor,
2657 '-m', commit_desc.description,
2658 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002659 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002660 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002661 if base_has_submodules:
2662 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2663 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2664 RunGit(['checkout', CHERRY_PICK_BRANCH])
2665 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002666 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002667 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002668 pending_prefix = settings.GetPendingRefPrefix()
2669 if not pending_prefix or branch.startswith(pending_prefix):
2670 # If not using refs/pending/heads/* at all, or target ref is already set
2671 # to pending, then push to the target ref directly.
2672 retcode, output = RunGitWithCode(
2673 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002674 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002675 else:
2676 # Cherry-pick the change on top of pending ref and then push it.
2677 assert branch.startswith('refs/'), branch
2678 assert pending_prefix[-1] == '/', pending_prefix
2679 pending_ref = pending_prefix + branch[len('refs/'):]
2680 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002681 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002682 if retcode == 0:
2683 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002684 else:
2685 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002686 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002687 'svn', 'dcommit',
2688 '-C%s' % options.similarity,
2689 '--no-rebase', '--rmdir',
2690 ]
2691 if settings.GetForceHttpsCommitUrl():
2692 # Allow forcing https commit URLs for some projects that don't allow
2693 # committing to http URLs (like Google Code).
2694 remote_url = cl.GetGitSvnRemoteUrl()
2695 if urlparse.urlparse(remote_url).scheme == 'http':
2696 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002697 cmd_args.append('--commit-url=%s' % remote_url)
2698 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002699 if 'Committed r' in output:
2700 revision = re.match(
2701 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2702 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002703 finally:
2704 # And then swap back to the original branch and clean up.
2705 RunGit(['checkout', '-q', cl.GetBranch()])
2706 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002707 if base_has_submodules:
2708 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002710 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002711 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002712 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002713
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002714 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002715 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002716 try:
2717 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2718 # We set pushed_to_pending to False, since it made it all the way to the
2719 # real ref.
2720 pushed_to_pending = False
2721 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002722 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002725 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002727 if not to_pending:
2728 if viewvc_url and revision:
2729 change_desc.append_footer(
2730 'Committed: %s%s' % (viewvc_url, revision))
2731 elif revision:
2732 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733 print ('Closing issue '
2734 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002735 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002737 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002738 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002739 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002740 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002741 if options.bypass_hooks:
2742 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2743 else:
2744 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002745 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002746 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002747
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002748 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002749 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2750 print 'The commit is in the pending queue (%s).' % pending_ref
2751 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002752 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002753 'footer.' % branch)
2754
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002755 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2756 if os.path.isfile(hook):
2757 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002758
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002759 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760
2761
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002762def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2763 print
2764 print 'Waiting for commit to be landed on %s...' % real_ref
2765 print '(If you are impatient, you may Ctrl-C once without harm)'
2766 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2767 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2768
2769 loop = 0
2770 while True:
2771 sys.stdout.write('fetching (%d)... \r' % loop)
2772 sys.stdout.flush()
2773 loop += 1
2774
2775 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2776 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2777 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2778 for commit in commits.splitlines():
2779 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2780 print 'Found commit on %s' % real_ref
2781 return commit
2782
2783 current_rev = to_rev
2784
2785
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002786def PushToGitPending(remote, pending_ref, upstream_ref):
2787 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2788
2789 Returns:
2790 (retcode of last operation, output log of last operation).
2791 """
2792 assert pending_ref.startswith('refs/'), pending_ref
2793 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2794 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2795 code = 0
2796 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002797 max_attempts = 3
2798 attempts_left = max_attempts
2799 while attempts_left:
2800 if attempts_left != max_attempts:
2801 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2802 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002803
2804 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002805 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002806 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002807 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002808 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002809 print 'Fetch failed with exit code %d.' % code
2810 if out.strip():
2811 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002812 continue
2813
2814 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002815 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002816 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002817 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002818 if code:
2819 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002820 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2821 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002822 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2823 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002824 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002825 return code, out
2826
2827 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002828 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002829 code, out = RunGitWithCode(
2830 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2831 if code == 0:
2832 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002833 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002834 return code, out
2835
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002836 print 'Push failed with exit code %d.' % code
2837 if out.strip():
2838 print out.strip()
2839 if IsFatalPushFailure(out):
2840 print (
2841 'Fatal push error. Make sure your .netrc credentials and git '
2842 'user.email are correct and you have push access to the repo.')
2843 return code, out
2844
2845 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002846 return code, out
2847
2848
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002849def IsFatalPushFailure(push_stdout):
2850 """True if retrying push won't help."""
2851 return '(prohibited by Gerrit)' in push_stdout
2852
2853
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002854@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002855def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002856 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002858 if get_footer_svn_id():
2859 # If it looks like previous commits were mirrored with git-svn.
2860 message = """This repository appears to be a git-svn mirror, but no
2861upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2862 else:
2863 message = """This doesn't appear to be an SVN repository.
2864If your project has a true, writeable git repository, you probably want to run
2865'git cl land' instead.
2866If your project has a git mirror of an upstream SVN master, you probably need
2867to run 'git svn init'.
2868
2869Using the wrong command might cause your commit to appear to succeed, and the
2870review to be closed, without actually landing upstream. If you choose to
2871proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002872 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002873 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874 return SendUpstream(parser, args, 'dcommit')
2875
2876
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002877@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002878def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002879 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002880 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881 print('This appears to be an SVN repository.')
2882 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002883 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002884 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002885 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002886
2887
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002888@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002890 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002891 parser.add_option('-b', dest='newbranch',
2892 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002893 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002895 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2896 help='Change to the directory DIR immediately, '
2897 'before doing anything else.')
2898 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002899 help='failed patches spew .rej files rather than '
2900 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002901 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2902 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002903 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002904 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002905 auth_config = auth.extract_auth_config_from_options(options)
2906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002907 if len(args) != 1:
2908 parser.print_help()
2909 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002910 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002912 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002913 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002914 return 1
2915
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002916 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002917 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002918
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002919 if options.newbranch:
2920 if options.force:
2921 RunGit(['branch', '-D', options.newbranch],
2922 stderr=subprocess2.PIPE, error_ok=True)
2923 RunGit(['checkout', '-b', options.newbranch,
2924 Changelist().GetUpstreamBranch()])
2925
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002926 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002927 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002928
2929
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002930def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002931 # PatchIssue should never be called with a dirty tree. It is up to the
2932 # caller to check this, but just in case we assert here since the
2933 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002934 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002935
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002936 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002937 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002938 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002939 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002940 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002941 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002943 # Assume it's a URL to the patch. Default to https.
2944 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002945 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002946 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 DieWithError('Must pass an issue ID or full URL for '
2948 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002949 issue = int(match.group(2))
2950 cl = Changelist(issue=issue, auth_config=auth_config)
2951 cl.rietveld_server = match.group(1)
2952 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002953 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955 # Switch up to the top-level directory, if necessary, in preparation for
2956 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002957 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958 if top:
2959 os.chdir(top)
2960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961 # Git patches have a/ at the beginning of source paths. We strip that out
2962 # with a sed script rather than the -p flag to patch so we can feed either
2963 # Git or svn-style patches into the same apply command.
2964 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002965 try:
2966 patch_data = subprocess2.check_output(
2967 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2968 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 DieWithError('Git patch mungling failed.')
2970 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 # We use "git apply" to apply the patch instead of "patch" so that we can
2973 # pick up file adds.
2974 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002975 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002976 if directory:
2977 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002978 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002979 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002980 elif IsGitVersionAtLeast('1.7.12'):
2981 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002982 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002983 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002984 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002985 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002986 print 'Failed to apply the patch'
2987 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988
2989 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002990 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002991 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2992 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002993 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2994 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002995 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002997 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002998 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999 else:
3000 print "Patch applied to index."
3001 return 0
3002
3003
3004def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003005 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006 # Provide a wrapper for git svn rebase to help avoid accidental
3007 # git svn dcommit.
3008 # It's the only command that doesn't use parser at all since we just defer
3009 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003010
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003011 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003012
3013
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003014def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003015 """Fetches the tree status and returns either 'open', 'closed',
3016 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003017 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003018 if url:
3019 status = urllib2.urlopen(url).read().lower()
3020 if status.find('closed') != -1 or status == '0':
3021 return 'closed'
3022 elif status.find('open') != -1 or status == '1':
3023 return 'open'
3024 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025 return 'unset'
3026
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003027
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003028def GetTreeStatusReason():
3029 """Fetches the tree status from a json url and returns the message
3030 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003031 url = settings.GetTreeStatusUrl()
3032 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033 connection = urllib2.urlopen(json_url)
3034 status = json.loads(connection.read())
3035 connection.close()
3036 return status['message']
3037
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003038
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003039def GetBuilderMaster(bot_list):
3040 """For a given builder, fetch the master from AE if available."""
3041 map_url = 'https://builders-map.appspot.com/'
3042 try:
3043 master_map = json.load(urllib2.urlopen(map_url))
3044 except urllib2.URLError as e:
3045 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3046 (map_url, e))
3047 except ValueError as e:
3048 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3049 if not master_map:
3050 return None, 'Failed to build master map.'
3051
3052 result_master = ''
3053 for bot in bot_list:
3054 builder = bot.split(':', 1)[0]
3055 master_list = master_map.get(builder, [])
3056 if not master_list:
3057 return None, ('No matching master for builder %s.' % builder)
3058 elif len(master_list) > 1:
3059 return None, ('The builder name %s exists in multiple masters %s.' %
3060 (builder, master_list))
3061 else:
3062 cur_master = master_list[0]
3063 if not result_master:
3064 result_master = cur_master
3065 elif result_master != cur_master:
3066 return None, 'The builders do not belong to the same master.'
3067 return result_master, None
3068
3069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003070def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003071 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003072 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073 status = GetTreeStatus()
3074 if 'unset' == status:
3075 print 'You must configure your tree status URL by running "git cl config".'
3076 return 2
3077
3078 print "The tree is %s" % status
3079 print
3080 print GetTreeStatusReason()
3081 if status != 'open':
3082 return 1
3083 return 0
3084
3085
maruel@chromium.org15192402012-09-06 12:38:29 +00003086def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003087 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003088 group = optparse.OptionGroup(parser, "Try job options")
3089 group.add_option(
3090 "-b", "--bot", action="append",
3091 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3092 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003093 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003094 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003095 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003096 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003097 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003098 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003099 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003100 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003101 "-r", "--revision",
3102 help="Revision to use for the try job; default: the "
3103 "revision will be determined by the try server; see "
3104 "its waterfall for more info")
3105 group.add_option(
3106 "-c", "--clobber", action="store_true", default=False,
3107 help="Force a clobber before building; e.g. don't do an "
3108 "incremental build")
3109 group.add_option(
3110 "--project",
3111 help="Override which project to use. Projects are defined "
3112 "server-side to define what default bot set to use")
3113 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003114 "-p", "--property", dest="properties", action="append", default=[],
3115 help="Specify generic properties in the form -p key1=value1 -p "
3116 "key2=value2 etc (buildbucket only). The value will be treated as "
3117 "json if decodable, or as string otherwise.")
3118 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003119 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003120 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003121 "--use-rietveld", action="store_true", default=False,
3122 help="Use Rietveld to trigger try jobs.")
3123 group.add_option(
3124 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3125 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003126 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003127 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003128 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003129 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003130
machenbach@chromium.org45453142015-09-15 08:45:22 +00003131 if options.use_rietveld and options.properties:
3132 parser.error('Properties can only be specified with buildbucket')
3133
3134 # Make sure that all properties are prop=value pairs.
3135 bad_params = [x for x in options.properties if '=' not in x]
3136 if bad_params:
3137 parser.error('Got properties with missing "=": %s' % bad_params)
3138
maruel@chromium.org15192402012-09-06 12:38:29 +00003139 if args:
3140 parser.error('Unknown arguments: %s' % args)
3141
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003142 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003143 if not cl.GetIssue():
3144 parser.error('Need to upload first')
3145
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003146 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003147 if props.get('closed'):
3148 parser.error('Cannot send tryjobs for a closed CL')
3149
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003150 if props.get('private'):
3151 parser.error('Cannot use trybots with private issue')
3152
maruel@chromium.org15192402012-09-06 12:38:29 +00003153 if not options.name:
3154 options.name = cl.GetBranch()
3155
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003156 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003157 options.master, err_msg = GetBuilderMaster(options.bot)
3158 if err_msg:
3159 parser.error('Tryserver master cannot be found because: %s\n'
3160 'Please manually specify the tryserver master'
3161 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003162
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003163 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003164 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003165 if not options.bot:
3166 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003167
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003168 # Get try masters from PRESUBMIT.py files.
3169 masters = presubmit_support.DoGetTryMasters(
3170 change,
3171 change.LocalPaths(),
3172 settings.GetRoot(),
3173 None,
3174 None,
3175 options.verbose,
3176 sys.stdout)
3177 if masters:
3178 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003179
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003180 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3181 options.bot = presubmit_support.DoGetTrySlaves(
3182 change,
3183 change.LocalPaths(),
3184 settings.GetRoot(),
3185 None,
3186 None,
3187 options.verbose,
3188 sys.stdout)
3189 if not options.bot:
3190 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003191
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003192 builders_and_tests = {}
3193 # TODO(machenbach): The old style command-line options don't support
3194 # multiple try masters yet.
3195 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3196 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3197
3198 for bot in old_style:
3199 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003200 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003201 elif ',' in bot:
3202 parser.error('Specify one bot per --bot flag')
3203 else:
3204 builders_and_tests.setdefault(bot, []).append('defaulttests')
3205
3206 for bot, tests in new_style:
3207 builders_and_tests.setdefault(bot, []).extend(tests)
3208
3209 # Return a master map with one master to be backwards compatible. The
3210 # master name defaults to an empty string, which will cause the master
3211 # not to be set on rietveld (deprecated).
3212 return {options.master: builders_and_tests}
3213
3214 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003215
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003216 for builders in masters.itervalues():
3217 if any('triggered' in b for b in builders):
3218 print >> sys.stderr, (
3219 'ERROR You are trying to send a job to a triggered bot. This type of'
3220 ' bot requires an\ninitial job from a parent (usually a builder). '
3221 'Instead send your job to the parent.\n'
3222 'Bot list: %s' % builders)
3223 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003224
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003225 patchset = cl.GetMostRecentPatchset()
3226 if patchset and patchset != cl.GetPatchset():
3227 print(
3228 '\nWARNING Mismatch between local config and server. Did a previous '
3229 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3230 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003231 if options.luci:
3232 trigger_luci_job(cl, masters, options)
3233 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003234 try:
3235 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3236 except BuildbucketResponseException as ex:
3237 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003238 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003239 except Exception as e:
3240 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3241 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3242 e, stacktrace)
3243 return 1
3244 else:
3245 try:
3246 cl.RpcServer().trigger_distributed_try_jobs(
3247 cl.GetIssue(), patchset, options.name, options.clobber,
3248 options.revision, masters)
3249 except urllib2.HTTPError as e:
3250 if e.code == 404:
3251 print('404 from rietveld; '
3252 'did you mean to use "git try" instead of "git cl try"?')
3253 return 1
3254 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003255
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003256 for (master, builders) in sorted(masters.iteritems()):
3257 if master:
3258 print 'Master: %s' % master
3259 length = max(len(builder) for builder in builders)
3260 for builder in sorted(builders):
3261 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003262 return 0
3263
3264
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003265@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003266def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003267 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003268 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003269 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003270 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003272 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003273 if args:
3274 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003275 branch = cl.GetBranch()
3276 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003277 cl = Changelist()
3278 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003279
3280 # Clear configured merge-base, if there is one.
3281 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003282 else:
3283 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003284 return 0
3285
3286
thestig@chromium.org00858c82013-12-02 23:08:03 +00003287def CMDweb(parser, args):
3288 """Opens the current CL in the web browser."""
3289 _, args = parser.parse_args(args)
3290 if args:
3291 parser.error('Unrecognized args: %s' % ' '.join(args))
3292
3293 issue_url = Changelist().GetIssueURL()
3294 if not issue_url:
3295 print >> sys.stderr, 'ERROR No issue to open'
3296 return 1
3297
3298 webbrowser.open(issue_url)
3299 return 0
3300
3301
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003302def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003303 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003304 auth.add_auth_options(parser)
3305 options, args = parser.parse_args(args)
3306 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003307 if args:
3308 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003309 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003310 props = cl.GetIssueProperties()
3311 if props.get('private'):
3312 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003313 cl.SetFlag('commit', '1')
3314 return 0
3315
3316
groby@chromium.org411034a2013-02-26 15:12:01 +00003317def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003318 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003319 auth.add_auth_options(parser)
3320 options, args = parser.parse_args(args)
3321 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003322 if args:
3323 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003324 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003325 # Ensure there actually is an issue to close.
3326 cl.GetDescription()
3327 cl.CloseIssue()
3328 return 0
3329
3330
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003331def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003332 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003333 auth.add_auth_options(parser)
3334 options, args = parser.parse_args(args)
3335 auth_config = auth.extract_auth_config_from_options(options)
3336 if args:
3337 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003338
3339 # Uncommitted (staged and unstaged) changes will be destroyed by
3340 # "git reset --hard" if there are merging conflicts in PatchIssue().
3341 # Staged changes would be committed along with the patch from last
3342 # upload, hence counted toward the "last upload" side in the final
3343 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003344 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003345 return 1
3346
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003347 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003348 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003349 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003350 if not issue:
3351 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003352 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003353 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003354
3355 # Create a new branch based on the merge-base
3356 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3357 try:
3358 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003359 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003360 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003361 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003362 return rtn
3363
wychen@chromium.org06928532015-02-03 02:11:29 +00003364 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003365 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003366 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003367 finally:
3368 RunGit(['checkout', '-q', branch])
3369 RunGit(['branch', '-D', TMP_BRANCH])
3370
3371 return 0
3372
3373
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003374def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003375 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003376 parser.add_option(
3377 '--no-color',
3378 action='store_true',
3379 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003380 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003381 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003382 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003383
3384 author = RunGit(['config', 'user.email']).strip() or None
3385
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003386 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003387
3388 if args:
3389 if len(args) > 1:
3390 parser.error('Unknown args')
3391 base_branch = args[0]
3392 else:
3393 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003394 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003395
3396 change = cl.GetChange(base_branch, None)
3397 return owners_finder.OwnersFinder(
3398 [f.LocalPath() for f in
3399 cl.GetChange(base_branch, None).AffectedFiles()],
3400 change.RepositoryRoot(), author,
3401 fopen=file, os_path=os.path, glob=glob.glob,
3402 disable_color=options.no_color).run()
3403
3404
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003405def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3406 """Generates a diff command."""
3407 # Generate diff for the current branch's changes.
3408 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3409 upstream_commit, '--' ]
3410
3411 if args:
3412 for arg in args:
3413 if os.path.isdir(arg):
3414 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3415 elif os.path.isfile(arg):
3416 diff_cmd.append(arg)
3417 else:
3418 DieWithError('Argument "%s" is not a file or a directory' % arg)
3419 else:
3420 diff_cmd.extend('*' + ext for ext in extensions)
3421
3422 return diff_cmd
3423
3424
enne@chromium.org555cfe42014-01-29 18:21:39 +00003425@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003426def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003427 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003428 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003429 parser.add_option('--full', action='store_true',
3430 help='Reformat the full content of all touched files')
3431 parser.add_option('--dry-run', action='store_true',
3432 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003433 parser.add_option('--python', action='store_true',
3434 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003435 parser.add_option('--diff', action='store_true',
3436 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003437 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003438
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003439 # git diff generates paths against the root of the repository. Change
3440 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003441 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003442 if rel_base_path:
3443 os.chdir(rel_base_path)
3444
digit@chromium.org29e47272013-05-17 17:01:46 +00003445 # Grab the merge-base commit, i.e. the upstream commit of the current
3446 # branch when it was created or the last time it was rebased. This is
3447 # to cover the case where the user may have called "git fetch origin",
3448 # moving the origin branch to a newer commit, but hasn't rebased yet.
3449 upstream_commit = None
3450 cl = Changelist()
3451 upstream_branch = cl.GetUpstreamBranch()
3452 if upstream_branch:
3453 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3454 upstream_commit = upstream_commit.strip()
3455
3456 if not upstream_commit:
3457 DieWithError('Could not find base commit for this branch. '
3458 'Are you in detached state?')
3459
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003460 if opts.full:
3461 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003462 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003463 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003464 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003465 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003466
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003467 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003468 diff_output = RunGit(diff_cmd)
3469
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003470 top_dir = os.path.normpath(
3471 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3472
3473 # Locate the clang-format binary in the checkout
3474 try:
3475 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3476 except clang_format.NotFoundError, e:
3477 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003478
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003479 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3480 # formatted. This is used to block during the presubmit.
3481 return_value = 0
3482
digit@chromium.org29e47272013-05-17 17:01:46 +00003483 if opts.full:
3484 # diff_output is a list of files to send to clang-format.
3485 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003486 if files:
3487 cmd = [clang_format_tool]
3488 if not opts.dry_run and not opts.diff:
3489 cmd.append('-i')
3490 stdout = RunCommand(cmd + files, cwd=top_dir)
3491 if opts.diff:
3492 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003493 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003494 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003495 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003496 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003497 try:
3498 script = clang_format.FindClangFormatScriptInChromiumTree(
3499 'clang-format-diff.py')
3500 except clang_format.NotFoundError, e:
3501 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003502
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003503 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003504 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003505 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003506
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003507 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003508 if opts.diff:
3509 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003510 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003511 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003512
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003513 # Similar code to above, but using yapf on .py files rather than clang-format
3514 # on C/C++ files
3515 if opts.python:
3516 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3517 diff_output = RunGit(diff_cmd)
3518 yapf_tool = gclient_utils.FindExecutable('yapf')
3519 if yapf_tool is None:
3520 DieWithError('yapf not found in PATH')
3521
3522 if opts.full:
3523 files = diff_output.splitlines()
3524 if files:
3525 cmd = [yapf_tool]
3526 if not opts.dry_run and not opts.diff:
3527 cmd.append('-i')
3528 stdout = RunCommand(cmd + files, cwd=top_dir)
3529 if opts.diff:
3530 sys.stdout.write(stdout)
3531 else:
3532 # TODO(sbc): yapf --lines mode still has some issues.
3533 # https://github.com/google/yapf/issues/154
3534 DieWithError('--python currently only works with --full')
3535
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003536 # Build a diff command that only operates on dart files. dart's formatter
3537 # does not have the nice property of only operating on modified chunks, so
3538 # hard code full.
3539 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3540 args, ['.dart'])
3541 dart_diff_output = RunGit(dart_diff_cmd)
3542 if dart_diff_output:
3543 try:
3544 command = [dart_format.FindDartFmtToolInChromiumTree()]
3545 if not opts.dry_run and not opts.diff:
3546 command.append('-w')
3547 command.extend(dart_diff_output.splitlines())
3548
3549 stdout = RunCommand(command, cwd=top_dir, env=env)
3550 if opts.dry_run and stdout:
3551 return_value = 2
3552 except dart_format.NotFoundError as e:
3553 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3554 'this checkout.')
3555
3556 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003557
3558
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003559@subcommand.usage('<codereview url or issue id>')
3560def CMDcheckout(parser, args):
3561 """Checks out a branch associated with a given Rietveld issue."""
3562 _, args = parser.parse_args(args)
3563
3564 if len(args) != 1:
3565 parser.print_help()
3566 return 1
3567
3568 if re.match(r'\d+', args[0]):
3569 target_issue = args[0]
3570 elif args[0].startswith('http'):
3571 target_issue = re.sub(r'.*/(\d+)/?', r'\1', args[0])
3572 else:
3573 parser.print_help()
3574 return 1
3575
3576 key_and_issues = [x.split() for x in RunGit(
3577 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3578 .splitlines()]
3579 branches = []
3580 for key, issue in key_and_issues:
3581 if issue == target_issue:
3582 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3583
3584 if len(branches) == 0:
3585 print 'No branch found for issue %s.' % target_issue
3586 return 1
3587 if len(branches) == 1:
3588 RunGit(['checkout', branches[0]])
3589 else:
3590 print 'Multiple branches match issue %s:' % target_issue
3591 for i in range(len(branches)):
3592 print '%d: %s' % (i, branches[i])
3593 which = raw_input('Choose by index: ')
3594 try:
3595 RunGit(['checkout', branches[int(which)]])
3596 except (IndexError, ValueError):
3597 print 'Invalid selection, not checking out any branch.'
3598 return 1
3599
3600 return 0
3601
3602
maruel@chromium.org29404b52014-09-08 22:58:00 +00003603def CMDlol(parser, args):
3604 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003605 print zlib.decompress(base64.b64decode(
3606 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3607 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3608 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3609 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003610 return 0
3611
3612
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003613class OptionParser(optparse.OptionParser):
3614 """Creates the option parse and add --verbose support."""
3615 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003616 optparse.OptionParser.__init__(
3617 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003618 self.add_option(
3619 '-v', '--verbose', action='count', default=0,
3620 help='Use 2 times for more debugging info')
3621
3622 def parse_args(self, args=None, values=None):
3623 options, args = optparse.OptionParser.parse_args(self, args, values)
3624 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3625 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3626 return options, args
3627
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003630 if sys.hexversion < 0x02060000:
3631 print >> sys.stderr, (
3632 '\nYour python version %s is unsupported, please upgrade.\n' %
3633 sys.version.split(' ', 1)[0])
3634 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003635
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003636 # Reload settings.
3637 global settings
3638 settings = Settings()
3639
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003640 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003641 dispatcher = subcommand.CommandDispatcher(__name__)
3642 try:
3643 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003644 except auth.AuthenticationError as e:
3645 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003646 except urllib2.HTTPError, e:
3647 if e.code != 500:
3648 raise
3649 DieWithError(
3650 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3651 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003652 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003653
3654
3655if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003656 # These affect sys.stdout so do it outside of main() to simplify mocks in
3657 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003658 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003659 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003660 try:
3661 sys.exit(main(sys.argv[1:]))
3662 except KeyboardInterrupt:
3663 sys.stderr.write('interrupted\n')
3664 sys.exit(1)