blob: 4964350b274285e3dad3883ecd84fefeca45080e [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
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000045import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000046import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000048from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000049import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000052import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000054import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000055import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000060DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000061POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000063GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000064CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000065REFS_THAT_ALIAS_TO_OTHER_REFS = {
66 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
67 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
68}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
thestig@chromium.org44202a22014-03-11 19:22:18 +000070# Valid extensions for files we want to lint.
71DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
72DEFAULT_LINT_IGNORE_REGEX = r"$^"
73
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000074# Shortcut since it quickly becomes redundant.
75Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000076
maruel@chromium.orgddd59412011-11-30 14:20:38 +000077# Initialized in main()
78settings = None
79
80
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000082 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083 sys.exit(1)
84
85
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000086def GetNoGitPagerEnv():
87 env = os.environ.copy()
88 # 'cat' is a magical git string that disables pagers on all platforms.
89 env['GIT_PAGER'] = 'cat'
90 return env
91
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000092
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000093def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000095 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000096 except subprocess2.CalledProcessError as e:
97 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 'Command "%s" failed.\n%s' % (
101 ' '.join(args), error_message or e.stdout or ''))
102 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103
104
105def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000107 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000110def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000112 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113 if suppress_stderr:
114 stderr = subprocess2.VOID
115 else:
116 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000117 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000118 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000119 stdout=subprocess2.PIPE,
120 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 return code, out[0]
122 except ValueError:
123 # When the subprocess fails, it returns None. That triggers a ValueError
124 # when trying to unpack the return value into (out, code).
125 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000126
127
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000128def RunGitSilent(args):
129 """Returns stdout, suppresses stderr and ingores the return code."""
130 return RunGitWithCode(args, suppress_stderr=True)[1]
131
132
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000133def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000134 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 return (version.startswith(prefix) and
137 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138
139
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000140def BranchExists(branch):
141 """Return True if specified branch exists."""
142 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
143 suppress_stderr=True)
144 return not code
145
146
maruel@chromium.org90541732011-04-01 17:54:18 +0000147def ask_for_data(prompt):
148 try:
149 return raw_input(prompt)
150 except KeyboardInterrupt:
151 # Hide the exception.
152 sys.exit(1)
153
154
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155def git_set_branch_value(key, value):
156 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000157 if not branch:
158 return
159
160 cmd = ['config']
161 if isinstance(value, int):
162 cmd.append('--int')
163 git_key = 'branch.%s.%s' % (branch, key)
164 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000165
166
167def git_get_branch_default(key, default):
168 branch = Changelist().GetBranch()
169 if branch:
170 git_key = 'branch.%s.%s' % (branch, key)
171 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
172 try:
173 return int(stdout.strip())
174 except ValueError:
175 pass
176 return default
177
178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000179def add_git_similarity(parser):
180 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000181 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182 help='Sets the percentage that a pair of files need to match in order to'
183 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 parser.add_option(
185 '--find-copies', action='store_true',
186 help='Allows git to look for copies.')
187 parser.add_option(
188 '--no-find-copies', action='store_false', dest='find_copies',
189 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000190
191 old_parser_args = parser.parse_args
192 def Parse(args):
193 options, args = old_parser_args(args)
194
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000196 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 print('Note: Saving similarity of %d%% in git config.'
199 % options.similarity)
200 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 options.similarity = max(0, min(options.similarity, 100))
203
204 if options.find_copies is None:
205 options.find_copies = bool(
206 git_get_branch_default('git-find-copies', True))
207 else:
208 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000209
210 print('Using %d%% similarity for rename/copy detection. '
211 'Override with --similarity.' % options.similarity)
212
213 return options, args
214 parser.parse_args = Parse
215
216
machenbach@chromium.org45453142015-09-15 08:45:22 +0000217def _get_properties_from_options(options):
218 properties = dict(x.split('=', 1) for x in options.properties)
219 for key, val in properties.iteritems():
220 try:
221 properties[key] = json.loads(val)
222 except ValueError:
223 pass # If a value couldn't be evaluated, treat it as a string.
224 return properties
225
226
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000227def _prefix_master(master):
228 """Convert user-specified master name to full master name.
229
230 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
231 name, while the developers always use shortened master name
232 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
233 function does the conversion for buildbucket migration.
234 """
235 prefix = 'master.'
236 if master.startswith(prefix):
237 return master
238 return '%s%s' % (prefix, master)
239
240
machenbach@chromium.org45453142015-09-15 08:45:22 +0000241def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000242 rietveld_url = settings.GetDefaultServerUrl()
243 rietveld_host = urlparse.urlparse(rietveld_url).hostname
244 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
245 http = authenticator.authorize(httplib2.Http())
246 http.force_exception_to_status_code = True
247 issue_props = changelist.GetIssueProperties()
248 issue = changelist.GetIssue()
249 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000250 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000251
252 buildbucket_put_url = (
253 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000254 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000255 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
256 hostname=rietveld_host,
257 issue=issue,
258 patch=patchset)
259
260 batch_req_body = {'builds': []}
261 print_text = []
262 print_text.append('Tried jobs on:')
263 for master, builders_and_tests in sorted(masters.iteritems()):
264 print_text.append('Master: %s' % master)
265 bucket = _prefix_master(master)
266 for builder, tests in sorted(builders_and_tests.iteritems()):
267 print_text.append(' %s: %s' % (builder, tests))
268 parameters = {
269 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000270 'changes': [{
271 'author': {'email': issue_props['owner_email']},
272 'revision': options.revision,
273 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000274 'properties': {
275 'category': category,
276 'issue': issue,
277 'master': master,
278 'patch_project': issue_props['project'],
279 'patch_storage': 'rietveld',
280 'patchset': patchset,
281 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000282 'rietveld': rietveld_url,
283 'testfilter': tests,
284 },
285 }
machenbach@chromium.org45453142015-09-15 08:45:22 +0000286 if properties:
287 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000288 if options.clobber:
289 parameters['properties']['clobber'] = True
290 batch_req_body['builds'].append(
291 {
292 'bucket': bucket,
293 'parameters_json': json.dumps(parameters),
294 'tags': ['builder:%s' % builder,
295 'buildset:%s' % buildset,
296 'master:%s' % master,
297 'user_agent:git_cl_try']
298 }
299 )
300
301 for try_count in xrange(3):
302 response, content = http.request(
303 buildbucket_put_url,
304 'PUT',
305 body=json.dumps(batch_req_body),
306 headers={'Content-Type': 'application/json'},
307 )
308 content_json = None
309 try:
310 content_json = json.loads(content)
311 except ValueError:
312 pass
313
314 # Buildbucket could return an error even if status==200.
315 if content_json and content_json.get('error'):
316 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
317 content_json['error'].get('code', ''),
318 content_json['error'].get('reason', ''),
319 content_json['error'].get('message', ''))
320 raise BuildbucketResponseException(msg)
321
322 if response.status == 200:
323 if not content_json:
324 raise BuildbucketResponseException(
325 'Buildbucket returns invalid json content: %s.\n'
326 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
327 content)
328 break
329 if response.status < 500 or try_count >= 2:
330 raise httplib2.HttpLib2Error(content)
331
332 # status >= 500 means transient failures.
333 logging.debug('Transient errors when triggering tryjobs. Will retry.')
334 time.sleep(0.5 + 1.5*try_count)
335
336 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000337
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000339def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
340 """Return the corresponding git ref if |base_url| together with |glob_spec|
341 matches the full |url|.
342
343 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
344 """
345 fetch_suburl, as_ref = glob_spec.split(':')
346 if allow_wildcards:
347 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
348 if glob_match:
349 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
350 # "branches/{472,597,648}/src:refs/remotes/svn/*".
351 branch_re = re.escape(base_url)
352 if glob_match.group(1):
353 branch_re += '/' + re.escape(glob_match.group(1))
354 wildcard = glob_match.group(2)
355 if wildcard == '*':
356 branch_re += '([^/]*)'
357 else:
358 # Escape and replace surrounding braces with parentheses and commas
359 # with pipe symbols.
360 wildcard = re.escape(wildcard)
361 wildcard = re.sub('^\\\\{', '(', wildcard)
362 wildcard = re.sub('\\\\,', '|', wildcard)
363 wildcard = re.sub('\\\\}$', ')', wildcard)
364 branch_re += wildcard
365 if glob_match.group(3):
366 branch_re += re.escape(glob_match.group(3))
367 match = re.match(branch_re, url)
368 if match:
369 return re.sub('\*$', match.group(1), as_ref)
370
371 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
372 if fetch_suburl:
373 full_url = base_url + '/' + fetch_suburl
374 else:
375 full_url = base_url
376 if full_url == url:
377 return as_ref
378 return None
379
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000380
iannucci@chromium.org79540052012-10-19 23:15:26 +0000381def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000382 """Prints statistics about the change to the user."""
383 # --no-ext-diff is broken in some versions of Git, so try to work around
384 # this by overriding the environment (but there is still a problem if the
385 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000386 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000387 if 'GIT_EXTERNAL_DIFF' in env:
388 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000389
390 if find_copies:
391 similarity_options = ['--find-copies-harder', '-l100000',
392 '-C%s' % similarity]
393 else:
394 similarity_options = ['-M%s' % similarity]
395
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000396 try:
397 stdout = sys.stdout.fileno()
398 except AttributeError:
399 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000400 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000401 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000402 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000403 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000404
405
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000406class BuildbucketResponseException(Exception):
407 pass
408
409
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410class Settings(object):
411 def __init__(self):
412 self.default_server = None
413 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000414 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 self.is_git_svn = None
416 self.svn_branch = None
417 self.tree_status_url = None
418 self.viewvc_url = None
419 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000420 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000421 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000422 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000423 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000424 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425
426 def LazyUpdateIfNeeded(self):
427 """Updates the settings from a codereview.settings file, if available."""
428 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000429 # The only value that actually changes the behavior is
430 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000431 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000432 error_ok=True
433 ).strip().lower()
434
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000436 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000438 # set updated to True to avoid infinite calling loop
439 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000440 self.updated = True
441 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 self.updated = True
443
444 def GetDefaultServerUrl(self, error_ok=False):
445 if not self.default_server:
446 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000447 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000448 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 if error_ok:
450 return self.default_server
451 if not self.default_server:
452 error_message = ('Could not find settings file. You must configure '
453 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000454 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000455 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456 return self.default_server
457
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000458 @staticmethod
459 def GetRelativeRoot():
460 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000463 if self.root is None:
464 self.root = os.path.abspath(self.GetRelativeRoot())
465 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466
467 def GetIsGitSvn(self):
468 """Return true if this repo looks like it's using git-svn."""
469 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000470 if self.GetPendingRefPrefix():
471 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
472 self.is_git_svn = False
473 else:
474 # If you have any "svn-remote.*" config keys, we think you're using svn.
475 self.is_git_svn = RunGitWithCode(
476 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 return self.is_git_svn
478
479 def GetSVNBranch(self):
480 if self.svn_branch is None:
481 if not self.GetIsGitSvn():
482 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
483
484 # Try to figure out which remote branch we're based on.
485 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000486 # 1) iterate through our branch history and find the svn URL.
487 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000488
489 # regexp matching the git-svn line that contains the URL.
490 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
491
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000492 # We don't want to go through all of history, so read a line from the
493 # pipe at a time.
494 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000495 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000496 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
497 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000498 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000499 for line in proc.stdout:
500 match = git_svn_re.match(line)
501 if match:
502 url = match.group(1)
503 proc.stdout.close() # Cut pipe.
504 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000506 if url:
507 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
508 remotes = RunGit(['config', '--get-regexp',
509 r'^svn-remote\..*\.url']).splitlines()
510 for remote in remotes:
511 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000512 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000513 remote = match.group(1)
514 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000515 rewrite_root = RunGit(
516 ['config', 'svn-remote.%s.rewriteRoot' % remote],
517 error_ok=True).strip()
518 if rewrite_root:
519 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000520 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000521 ['config', 'svn-remote.%s.fetch' % remote],
522 error_ok=True).strip()
523 if fetch_spec:
524 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
525 if self.svn_branch:
526 break
527 branch_spec = RunGit(
528 ['config', 'svn-remote.%s.branches' % remote],
529 error_ok=True).strip()
530 if branch_spec:
531 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
532 if self.svn_branch:
533 break
534 tag_spec = RunGit(
535 ['config', 'svn-remote.%s.tags' % remote],
536 error_ok=True).strip()
537 if tag_spec:
538 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
539 if self.svn_branch:
540 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000541
542 if not self.svn_branch:
543 DieWithError('Can\'t guess svn branch -- try specifying it on the '
544 'command line')
545
546 return self.svn_branch
547
548 def GetTreeStatusUrl(self, error_ok=False):
549 if not self.tree_status_url:
550 error_message = ('You must configure your tree status URL by running '
551 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000552 self.tree_status_url = self._GetRietveldConfig(
553 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000554 return self.tree_status_url
555
556 def GetViewVCUrl(self):
557 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000558 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559 return self.viewvc_url
560
rmistry@google.com90752582014-01-14 21:04:50 +0000561 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000562 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000563
rmistry@google.com78948ed2015-07-08 23:09:57 +0000564 def GetIsSkipDependencyUpload(self, branch_name):
565 """Returns true if specified branch should skip dep uploads."""
566 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
567 error_ok=True)
568
rmistry@google.com5626a922015-02-26 14:03:30 +0000569 def GetRunPostUploadHook(self):
570 run_post_upload_hook = self._GetRietveldConfig(
571 'run-post-upload-hook', error_ok=True)
572 return run_post_upload_hook == "True"
573
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000574 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000575 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000576
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000577 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000578 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000579
ukai@chromium.orge8077812012-02-03 03:41:46 +0000580 def GetIsGerrit(self):
581 """Return true if this repo is assosiated with gerrit code review system."""
582 if self.is_gerrit is None:
583 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
584 return self.is_gerrit
585
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000586 def GetGitEditor(self):
587 """Return the editor specified in the git config, or None if none is."""
588 if self.git_editor is None:
589 self.git_editor = self._GetConfig('core.editor', error_ok=True)
590 return self.git_editor or None
591
thestig@chromium.org44202a22014-03-11 19:22:18 +0000592 def GetLintRegex(self):
593 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
594 DEFAULT_LINT_REGEX)
595
596 def GetLintIgnoreRegex(self):
597 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
598 DEFAULT_LINT_IGNORE_REGEX)
599
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000600 def GetProject(self):
601 if not self.project:
602 self.project = self._GetRietveldConfig('project', error_ok=True)
603 return self.project
604
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000605 def GetForceHttpsCommitUrl(self):
606 if not self.force_https_commit_url:
607 self.force_https_commit_url = self._GetRietveldConfig(
608 'force-https-commit-url', error_ok=True)
609 return self.force_https_commit_url
610
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000611 def GetPendingRefPrefix(self):
612 if not self.pending_ref_prefix:
613 self.pending_ref_prefix = self._GetRietveldConfig(
614 'pending-ref-prefix', error_ok=True)
615 return self.pending_ref_prefix
616
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000617 def _GetRietveldConfig(self, param, **kwargs):
618 return self._GetConfig('rietveld.' + param, **kwargs)
619
rmistry@google.com78948ed2015-07-08 23:09:57 +0000620 def _GetBranchConfig(self, branch_name, param, **kwargs):
621 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 def _GetConfig(self, param, **kwargs):
624 self.LazyUpdateIfNeeded()
625 return RunGit(['config', param], **kwargs).strip()
626
627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628def ShortBranchName(branch):
629 """Convert a name like 'refs/heads/foo' to just 'foo'."""
630 return branch.replace('refs/heads/', '')
631
632
633class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000634 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000636 global settings
637 if not settings:
638 # Happens when git_cl.py is used as a utility library.
639 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640 settings.GetDefaultServerUrl()
641 self.branchref = branchref
642 if self.branchref:
643 self.branch = ShortBranchName(self.branchref)
644 else:
645 self.branch = None
646 self.rietveld_server = None
647 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000648 self.lookedup_issue = False
649 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 self.has_description = False
651 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000652 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000654 self.cc = None
655 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000656 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000657 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000658 self._remote = None
659 self._rpc_server = None
660
661 @property
662 def auth_config(self):
663 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000664
665 def GetCCList(self):
666 """Return the users cc'd on this CL.
667
668 Return is a string suitable for passing to gcl with the --cc flag.
669 """
670 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000671 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000672 more_cc = ','.join(self.watchers)
673 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
674 return self.cc
675
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000676 def GetCCListWithoutDefault(self):
677 """Return the users cc'd on this CL excluding default ones."""
678 if self.cc is None:
679 self.cc = ','.join(self.watchers)
680 return self.cc
681
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000682 def SetWatchers(self, watchers):
683 """Set the list of email addresses that should be cc'd based on the changed
684 files in this CL.
685 """
686 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687
688 def GetBranch(self):
689 """Returns the short branch name, e.g. 'master'."""
690 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000691 branchref = RunGit(['symbolic-ref', 'HEAD'],
692 stderr=subprocess2.VOID, error_ok=True).strip()
693 if not branchref:
694 return None
695 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 self.branch = ShortBranchName(self.branchref)
697 return self.branch
698
699 def GetBranchRef(self):
700 """Returns the full branch name, e.g. 'refs/heads/master'."""
701 self.GetBranch() # Poke the lazy loader.
702 return self.branchref
703
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000704 @staticmethod
705 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000706 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 e.g. 'origin', 'refs/heads/master'
708 """
709 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
711 error_ok=True).strip()
712 if upstream_branch:
713 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
714 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000715 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
716 error_ok=True).strip()
717 if upstream_branch:
718 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000720 # Fall back on trying a git-svn upstream branch.
721 if settings.GetIsGitSvn():
722 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000724 # Else, try to guess the origin remote.
725 remote_branches = RunGit(['branch', '-r']).split()
726 if 'origin/master' in remote_branches:
727 # Fall back on origin/master if it exits.
728 remote = 'origin'
729 upstream_branch = 'refs/heads/master'
730 elif 'origin/trunk' in remote_branches:
731 # Fall back on origin/trunk if it exists. Generally a shared
732 # git-svn clone
733 remote = 'origin'
734 upstream_branch = 'refs/heads/trunk'
735 else:
736 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737Either pass complete "git diff"-style arguments, like
738 git cl upload origin/master
739or verify this branch is set up to track another (via the --track argument to
740"git checkout -b ...").""")
741
742 return remote, upstream_branch
743
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000745 upstream_branch = self.GetUpstreamBranch()
746 if not BranchExists(upstream_branch):
747 DieWithError('The upstream for the current branch (%s) does not exist '
748 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000749 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000750 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 def GetUpstreamBranch(self):
753 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000754 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000756 upstream_branch = upstream_branch.replace('refs/heads/',
757 'refs/remotes/%s/' % remote)
758 upstream_branch = upstream_branch.replace('refs/branch-heads/',
759 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 self.upstream_branch = upstream_branch
761 return self.upstream_branch
762
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000763 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000764 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000765 remote, branch = None, self.GetBranch()
766 seen_branches = set()
767 while branch not in seen_branches:
768 seen_branches.add(branch)
769 remote, branch = self.FetchUpstreamTuple(branch)
770 branch = ShortBranchName(branch)
771 if remote != '.' or branch.startswith('refs/remotes'):
772 break
773 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000774 remotes = RunGit(['remote'], error_ok=True).split()
775 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000776 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000777 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000778 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000779 logging.warning('Could not determine which remote this change is '
780 'associated with, so defaulting to "%s". This may '
781 'not be what you want. You may prevent this message '
782 'by running "git svn info" as documented here: %s',
783 self._remote,
784 GIT_INSTRUCTIONS_URL)
785 else:
786 logging.warn('Could not determine which remote this change is '
787 'associated with. You may prevent this message by '
788 'running "git svn info" as documented here: %s',
789 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000790 branch = 'HEAD'
791 if branch.startswith('refs/remotes'):
792 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000793 elif branch.startswith('refs/branch-heads/'):
794 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000795 else:
796 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000797 return self._remote
798
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000799 def GitSanityChecks(self, upstream_git_obj):
800 """Checks git repo status and ensures diff is from local commits."""
801
sbc@chromium.org79706062015-01-14 21:18:12 +0000802 if upstream_git_obj is None:
803 if self.GetBranch() is None:
804 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000805 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000806 else:
807 print >> sys.stderr, (
808 'ERROR: no upstream branch')
809 return False
810
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000811 # Verify the commit we're diffing against is in our current branch.
812 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
813 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
814 if upstream_sha != common_ancestor:
815 print >> sys.stderr, (
816 'ERROR: %s is not in the current branch. You may need to rebase '
817 'your tracking branch' % upstream_sha)
818 return False
819
820 # List the commits inside the diff, and verify they are all local.
821 commits_in_diff = RunGit(
822 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
823 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
824 remote_branch = remote_branch.strip()
825 if code != 0:
826 _, remote_branch = self.GetRemoteBranch()
827
828 commits_in_remote = RunGit(
829 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
830
831 common_commits = set(commits_in_diff) & set(commits_in_remote)
832 if common_commits:
833 print >> sys.stderr, (
834 'ERROR: Your diff contains %d commits already in %s.\n'
835 'Run "git log --oneline %s..HEAD" to get a list of commits in '
836 'the diff. If you are using a custom git flow, you can override'
837 ' the reference used for this check with "git config '
838 'gitcl.remotebranch <git-ref>".' % (
839 len(common_commits), remote_branch, upstream_git_obj))
840 return False
841 return True
842
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000843 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000844 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000845
846 Returns None if it is not set.
847 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000848 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
849 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000850
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000851 def GetGitSvnRemoteUrl(self):
852 """Return the configured git-svn remote URL parsed from git svn info.
853
854 Returns None if it is not set.
855 """
856 # URL is dependent on the current directory.
857 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
858 if data:
859 keys = dict(line.split(': ', 1) for line in data.splitlines()
860 if ': ' in line)
861 return keys.get('URL', None)
862 return None
863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 def GetRemoteUrl(self):
865 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
866
867 Returns None if there is no remote.
868 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000869 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000870 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
871
872 # If URL is pointing to a local directory, it is probably a git cache.
873 if os.path.isdir(url):
874 url = RunGit(['config', 'remote.%s.url' % remote],
875 error_ok=True,
876 cwd=url).strip()
877 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
879 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000880 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000881 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000883 self.issue = int(issue) or None if issue else None
884 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 return self.issue
886
887 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000888 if not self.rietveld_server:
889 # If we're on a branch then get the server potentially associated
890 # with that branch.
891 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000892 rietveld_server_config = self._RietveldServer()
893 if rietveld_server_config:
894 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
895 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000896 if not self.rietveld_server:
897 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 return self.rietveld_server
899
900 def GetIssueURL(self):
901 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000902 if not self.GetIssue():
903 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
905
906 def GetDescription(self, pretty=False):
907 if not self.has_description:
908 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000909 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000910 try:
911 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000912 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000913 if e.code == 404:
914 DieWithError(
915 ('\nWhile fetching the description for issue %d, received a '
916 '404 (not found)\n'
917 'error. It is likely that you deleted this '
918 'issue on the server. If this is the\n'
919 'case, please run\n\n'
920 ' git cl issue 0\n\n'
921 'to clear the association with the deleted issue. Then run '
922 'this command again.') % issue)
923 else:
924 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000925 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000926 except urllib2.URLError as e:
927 print >> sys.stderr, (
928 'Warning: Failed to retrieve CL description due to network '
929 'failure.')
930 self.description = ''
931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 self.has_description = True
933 if pretty:
934 wrapper = textwrap.TextWrapper()
935 wrapper.initial_indent = wrapper.subsequent_indent = ' '
936 return wrapper.fill(self.description)
937 return self.description
938
939 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000940 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000941 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 patchset = RunGit(['config', self._PatchsetSetting()],
943 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000944 self.patchset = int(patchset) or None if patchset else None
945 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 return self.patchset
947
948 def SetPatchset(self, patchset):
949 """Set this branch's patchset. If patchset=0, clears the patchset."""
950 if patchset:
951 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000952 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 else:
954 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000955 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000958 def GetMostRecentPatchset(self):
959 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000960
961 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000962 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000963 '/download/issue%s_%s.diff' % (issue, patchset))
964
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000965 def GetIssueProperties(self):
966 if self._props is None:
967 issue = self.GetIssue()
968 if not issue:
969 self._props = {}
970 else:
971 self._props = self.RpcServer().get_issue_properties(issue, True)
972 return self._props
973
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000974 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000975 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000976
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000977 def AddComment(self, message):
978 return self.RpcServer().add_comment(self.GetIssue(), message)
979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 def SetIssue(self, issue):
981 """Set this branch's issue. If issue=0, clears the issue."""
982 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000983 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 RunGit(['config', self._IssueSetting(), str(issue)])
985 if self.rietveld_server:
986 RunGit(['config', self._RietveldServer(), self.rietveld_server])
987 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000988 current_issue = self.GetIssue()
989 if current_issue:
990 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000991 self.issue = None
992 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000994 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000995 if not self.GitSanityChecks(upstream_branch):
996 DieWithError('\nGit sanity check failure')
997
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000998 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000999 if not root:
1000 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001001 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001002
1003 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001004 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001005 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001006 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001007 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001008 except subprocess2.CalledProcessError:
1009 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001010 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001011 'This branch probably doesn\'t exist anymore. To reset the\n'
1012 'tracking branch, please run\n'
1013 ' git branch --set-upstream %s trunk\n'
1014 'replacing trunk with origin/master or the relevant branch') %
1015 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001016
maruel@chromium.org52424302012-08-29 15:14:30 +00001017 issue = self.GetIssue()
1018 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001019 if issue:
1020 description = self.GetDescription()
1021 else:
1022 # If the change was never uploaded, use the log messages of all commits
1023 # up to the branch point, as git cl upload will prefill the description
1024 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001025 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1026 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001027
1028 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001029 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001030 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001031 name,
1032 description,
1033 absroot,
1034 files,
1035 issue,
1036 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001037 author,
1038 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001039
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001040 def GetStatus(self):
1041 """Apply a rough heuristic to give a simple summary of an issue's review
1042 or CQ status, assuming adherence to a common workflow.
1043
1044 Returns None if no issue for this branch, or one of the following keywords:
1045 * 'error' - error from review tool (including deleted issues)
1046 * 'unsent' - not sent for review
1047 * 'waiting' - waiting for review
1048 * 'reply' - waiting for owner to reply to review
1049 * 'lgtm' - LGTM from at least one approved reviewer
1050 * 'commit' - in the commit queue
1051 * 'closed' - closed
1052 """
1053 if not self.GetIssue():
1054 return None
1055
1056 try:
1057 props = self.GetIssueProperties()
1058 except urllib2.HTTPError:
1059 return 'error'
1060
1061 if props.get('closed'):
1062 # Issue is closed.
1063 return 'closed'
1064 if props.get('commit'):
1065 # Issue is in the commit queue.
1066 return 'commit'
1067
1068 try:
1069 reviewers = self.GetApprovingReviewers()
1070 except urllib2.HTTPError:
1071 return 'error'
1072
1073 if reviewers:
1074 # Was LGTM'ed.
1075 return 'lgtm'
1076
1077 messages = props.get('messages') or []
1078
1079 if not messages:
1080 # No message was sent.
1081 return 'unsent'
1082 if messages[-1]['sender'] != props.get('owner_email'):
1083 # Non-LGTM reply from non-owner
1084 return 'reply'
1085 return 'waiting'
1086
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001087 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001088 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001089
1090 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001091 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001092 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001093 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001094 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001095 except presubmit_support.PresubmitFailure, e:
1096 DieWithError(
1097 ('%s\nMaybe your depot_tools is out of date?\n'
1098 'If all fails, contact maruel@') % e)
1099
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001100 def UpdateDescription(self, description):
1101 self.description = description
1102 return self.RpcServer().update_description(
1103 self.GetIssue(), self.description)
1104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001106 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001107 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001109 def SetFlag(self, flag, value):
1110 """Patchset must match."""
1111 if not self.GetPatchset():
1112 DieWithError('The patchset needs to match. Send another patchset.')
1113 try:
1114 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001115 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001116 except urllib2.HTTPError, e:
1117 if e.code == 404:
1118 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1119 if e.code == 403:
1120 DieWithError(
1121 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1122 'match?') % (self.GetIssue(), self.GetPatchset()))
1123 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001125 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 """Returns an upload.RpcServer() to access this review's rietveld instance.
1127 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001128 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001129 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001130 self.GetRietveldServer(),
1131 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001132 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
1134 def _IssueSetting(self):
1135 """Return the git setting that stores this change's issue."""
1136 return 'branch.%s.rietveldissue' % self.GetBranch()
1137
1138 def _PatchsetSetting(self):
1139 """Return the git setting that stores this change's most recent patchset."""
1140 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1141
1142 def _RietveldServer(self):
1143 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001144 branch = self.GetBranch()
1145 if branch:
1146 return 'branch.%s.rietveldserver' % branch
1147 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
1149
1150def GetCodereviewSettingsInteractively():
1151 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001152 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 server = settings.GetDefaultServerUrl(error_ok=True)
1154 prompt = 'Rietveld server (host[:port])'
1155 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001156 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 if not server and not newserver:
1158 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001159 if newserver:
1160 newserver = gclient_utils.UpgradeToHttps(newserver)
1161 if newserver != server:
1162 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001164 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 prompt = caption
1166 if initial:
1167 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001168 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if new_val == 'x':
1170 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001171 elif new_val:
1172 if is_url:
1173 new_val = gclient_utils.UpgradeToHttps(new_val)
1174 if new_val != initial:
1175 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001177 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001178 SetProperty(settings.GetDefaultPrivateFlag(),
1179 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001181 'tree-status-url', False)
1182 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001183 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001184 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1185 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
1187 # TODO: configure a default branch to diff against, rather than this
1188 # svn-based hackery.
1189
1190
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001191class ChangeDescription(object):
1192 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001193 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001194 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001195
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001196 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001197 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001198
agable@chromium.org42c20792013-09-12 17:34:49 +00001199 @property # www.logilab.org/ticket/89786
1200 def description(self): # pylint: disable=E0202
1201 return '\n'.join(self._description_lines)
1202
1203 def set_description(self, desc):
1204 if isinstance(desc, basestring):
1205 lines = desc.splitlines()
1206 else:
1207 lines = [line.rstrip() for line in desc]
1208 while lines and not lines[0]:
1209 lines.pop(0)
1210 while lines and not lines[-1]:
1211 lines.pop(-1)
1212 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001213
piman@chromium.org336f9122014-09-04 02:16:55 +00001214 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001215 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001216 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001217 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001218 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001219 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001220
agable@chromium.org42c20792013-09-12 17:34:49 +00001221 # Get the set of R= and TBR= lines and remove them from the desciption.
1222 regexp = re.compile(self.R_LINE)
1223 matches = [regexp.match(line) for line in self._description_lines]
1224 new_desc = [l for i, l in enumerate(self._description_lines)
1225 if not matches[i]]
1226 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001227
agable@chromium.org42c20792013-09-12 17:34:49 +00001228 # Construct new unified R= and TBR= lines.
1229 r_names = []
1230 tbr_names = []
1231 for match in matches:
1232 if not match:
1233 continue
1234 people = cleanup_list([match.group(2).strip()])
1235 if match.group(1) == 'TBR':
1236 tbr_names.extend(people)
1237 else:
1238 r_names.extend(people)
1239 for name in r_names:
1240 if name not in reviewers:
1241 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001242 if add_owners_tbr:
1243 owners_db = owners.Database(change.RepositoryRoot(),
1244 fopen=file, os_path=os.path, glob=glob.glob)
1245 all_reviewers = set(tbr_names + reviewers)
1246 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1247 all_reviewers)
1248 tbr_names.extend(owners_db.reviewers_for(missing_files,
1249 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001250 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1251 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1252
1253 # Put the new lines in the description where the old first R= line was.
1254 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1255 if 0 <= line_loc < len(self._description_lines):
1256 if new_tbr_line:
1257 self._description_lines.insert(line_loc, new_tbr_line)
1258 if new_r_line:
1259 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001260 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 if new_r_line:
1262 self.append_footer(new_r_line)
1263 if new_tbr_line:
1264 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001265
1266 def prompt(self):
1267 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001268 self.set_description([
1269 '# Enter a description of the change.',
1270 '# This will be displayed on the codereview site.',
1271 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001272 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001273 '--------------------',
1274 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001275
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 regexp = re.compile(self.BUG_LINE)
1277 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001278 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001279 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001280 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001281 if not content:
1282 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001283 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001284
1285 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001286 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1287 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001288 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001289 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001290
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001291 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001292 if self._description_lines:
1293 # Add an empty line if either the last line or the new line isn't a tag.
1294 last_line = self._description_lines[-1]
1295 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1296 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1297 self._description_lines.append('')
1298 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001299
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001300 def get_reviewers(self):
1301 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001302 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1303 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001304 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001305
1306
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001307def get_approving_reviewers(props):
1308 """Retrieves the reviewers that approved a CL from the issue properties with
1309 messages.
1310
1311 Note that the list may contain reviewers that are not committer, thus are not
1312 considered by the CQ.
1313 """
1314 return sorted(
1315 set(
1316 message['sender']
1317 for message in props['messages']
1318 if message['approval'] and message['sender'] in props['reviewers']
1319 )
1320 )
1321
1322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323def FindCodereviewSettingsFile(filename='codereview.settings'):
1324 """Finds the given file starting in the cwd and going up.
1325
1326 Only looks up to the top of the repository unless an
1327 'inherit-review-settings-ok' file exists in the root of the repository.
1328 """
1329 inherit_ok_file = 'inherit-review-settings-ok'
1330 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001331 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1333 root = '/'
1334 while True:
1335 if filename in os.listdir(cwd):
1336 if os.path.isfile(os.path.join(cwd, filename)):
1337 return open(os.path.join(cwd, filename))
1338 if cwd == root:
1339 break
1340 cwd = os.path.dirname(cwd)
1341
1342
1343def LoadCodereviewSettingsFromFile(fileobj):
1344 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001345 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 def SetProperty(name, setting, unset_error_ok=False):
1348 fullname = 'rietveld.' + name
1349 if setting in keyvals:
1350 RunGit(['config', fullname, keyvals[setting]])
1351 else:
1352 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1353
1354 SetProperty('server', 'CODE_REVIEW_SERVER')
1355 # Only server setting is required. Other settings can be absent.
1356 # In that case, we ignore errors raised during option deletion attempt.
1357 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001358 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1360 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001361 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001362 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001363 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1364 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001365 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001366 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001367 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001368 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1369 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001371 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001372 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001373
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1375 #should be of the form
1376 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1377 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1378 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1379 keyvals['ORIGIN_URL_CONFIG']])
1380
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001382def urlretrieve(source, destination):
1383 """urllib is broken for SSL connections via a proxy therefore we
1384 can't use urllib.urlretrieve()."""
1385 with open(destination, 'w') as f:
1386 f.write(urllib2.urlopen(source).read())
1387
1388
ukai@chromium.org712d6102013-11-27 00:52:58 +00001389def hasSheBang(fname):
1390 """Checks fname is a #! script."""
1391 with open(fname) as f:
1392 return f.read(2).startswith('#!')
1393
1394
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001395def DownloadHooks(force):
1396 """downloads hooks
1397
1398 Args:
1399 force: True to update hooks. False to install hooks if not present.
1400 """
1401 if not settings.GetIsGerrit():
1402 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001403 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001404 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1405 if not os.access(dst, os.X_OK):
1406 if os.path.exists(dst):
1407 if not force:
1408 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001409 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001410 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001411 if not hasSheBang(dst):
1412 DieWithError('Not a script: %s\n'
1413 'You need to download from\n%s\n'
1414 'into .git/hooks/commit-msg and '
1415 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001416 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1417 except Exception:
1418 if os.path.exists(dst):
1419 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001420 DieWithError('\nFailed to download hooks.\n'
1421 'You need to download from\n%s\n'
1422 'into .git/hooks/commit-msg and '
1423 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001424
1425
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001426@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001428 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001430 parser.add_option('--activate-update', action='store_true',
1431 help='activate auto-updating [rietveld] section in '
1432 '.git/config')
1433 parser.add_option('--deactivate-update', action='store_true',
1434 help='deactivate auto-updating [rietveld] section in '
1435 '.git/config')
1436 options, args = parser.parse_args(args)
1437
1438 if options.deactivate_update:
1439 RunGit(['config', 'rietveld.autoupdate', 'false'])
1440 return
1441
1442 if options.activate_update:
1443 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1444 return
1445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 if len(args) == 0:
1447 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001448 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 return 0
1450
1451 url = args[0]
1452 if not url.endswith('codereview.settings'):
1453 url = os.path.join(url, 'codereview.settings')
1454
1455 # Load code review settings and download hooks (if available).
1456 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001457 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 return 0
1459
1460
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001461def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001462 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001463 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1464 branch = ShortBranchName(branchref)
1465 _, args = parser.parse_args(args)
1466 if not args:
1467 print("Current base-url:")
1468 return RunGit(['config', 'branch.%s.base-url' % branch],
1469 error_ok=False).strip()
1470 else:
1471 print("Setting base-url to %s" % args[0])
1472 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1473 error_ok=False).strip()
1474
1475
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001476def color_for_status(status):
1477 """Maps a Changelist status to color, for CMDstatus and other tools."""
1478 return {
1479 'unsent': Fore.RED,
1480 'waiting': Fore.BLUE,
1481 'reply': Fore.YELLOW,
1482 'lgtm': Fore.GREEN,
1483 'commit': Fore.MAGENTA,
1484 'closed': Fore.CYAN,
1485 'error': Fore.WHITE,
1486 }.get(status, Fore.WHITE)
1487
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001488def fetch_cl_status(branch, auth_config=None):
1489 """Fetches information for an issue and returns (branch, issue, status)."""
1490 cl = Changelist(branchref=branch, auth_config=auth_config)
1491 url = cl.GetIssueURL()
1492 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001493
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001494 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001495 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001496 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001497
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001498 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001499
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001500def get_cl_statuses(
1501 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001502 """Returns a blocking iterable of (branch, issue, color) for given branches.
1503
1504 If fine_grained is true, this will fetch CL statuses from the server.
1505 Otherwise, simply indicate if there's a matching url for the given branches.
1506
1507 If max_processes is specified, it is used as the maximum number of processes
1508 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1509 spawned.
1510 """
1511 # Silence upload.py otherwise it becomes unwieldly.
1512 upload.verbosity = 0
1513
1514 if fine_grained:
1515 # Process one branch synchronously to work through authentication, then
1516 # spawn processes to process all the other branches in parallel.
1517 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001518 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1519 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001520
1521 branches_to_fetch = branches[1:]
1522 pool = ThreadPool(
1523 min(max_processes, len(branches_to_fetch))
1524 if max_processes is not None
1525 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001526 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001527 yield x
1528 else:
1529 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1530 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001531 cl = Changelist(branchref=b, auth_config=auth_config)
1532 url = cl.GetIssueURL()
1533 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001534
rmistry@google.com2dd99862015-06-22 12:22:18 +00001535
1536def upload_branch_deps(cl, args):
1537 """Uploads CLs of local branches that are dependents of the current branch.
1538
1539 If the local branch dependency tree looks like:
1540 test1 -> test2.1 -> test3.1
1541 -> test3.2
1542 -> test2.2 -> test3.3
1543
1544 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1545 run on the dependent branches in this order:
1546 test2.1, test3.1, test3.2, test2.2, test3.3
1547
1548 Note: This function does not rebase your local dependent branches. Use it when
1549 you make a change to the parent branch that will not conflict with its
1550 dependent branches, and you would like their dependencies updated in
1551 Rietveld.
1552 """
1553 if git_common.is_dirty_git_tree('upload-branch-deps'):
1554 return 1
1555
1556 root_branch = cl.GetBranch()
1557 if root_branch is None:
1558 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1559 'Get on a branch!')
1560 if not cl.GetIssue() or not cl.GetPatchset():
1561 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1562 'patchset dependencies without an uploaded CL.')
1563
1564 branches = RunGit(['for-each-ref',
1565 '--format=%(refname:short) %(upstream:short)',
1566 'refs/heads'])
1567 if not branches:
1568 print('No local branches found.')
1569 return 0
1570
1571 # Create a dictionary of all local branches to the branches that are dependent
1572 # on it.
1573 tracked_to_dependents = collections.defaultdict(list)
1574 for b in branches.splitlines():
1575 tokens = b.split()
1576 if len(tokens) == 2:
1577 branch_name, tracked = tokens
1578 tracked_to_dependents[tracked].append(branch_name)
1579
1580 print
1581 print 'The dependent local branches of %s are:' % root_branch
1582 dependents = []
1583 def traverse_dependents_preorder(branch, padding=''):
1584 dependents_to_process = tracked_to_dependents.get(branch, [])
1585 padding += ' '
1586 for dependent in dependents_to_process:
1587 print '%s%s' % (padding, dependent)
1588 dependents.append(dependent)
1589 traverse_dependents_preorder(dependent, padding)
1590 traverse_dependents_preorder(root_branch)
1591 print
1592
1593 if not dependents:
1594 print 'There are no dependent local branches for %s' % root_branch
1595 return 0
1596
1597 print ('This command will checkout all dependent branches and run '
1598 '"git cl upload".')
1599 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1600
1601 # Add a default patchset title to all upload calls.
1602 args.extend(['-t', 'Updated patchset dependency'])
1603 # Record all dependents that failed to upload.
1604 failures = {}
1605 # Go through all dependents, checkout the branch and upload.
1606 try:
1607 for dependent_branch in dependents:
1608 print
1609 print '--------------------------------------'
1610 print 'Running "git cl upload" from %s:' % dependent_branch
1611 RunGit(['checkout', '-q', dependent_branch])
1612 print
1613 try:
1614 if CMDupload(OptionParser(), args) != 0:
1615 print 'Upload failed for %s!' % dependent_branch
1616 failures[dependent_branch] = 1
1617 except: # pylint: disable=W0702
1618 failures[dependent_branch] = 1
1619 print
1620 finally:
1621 # Swap back to the original root branch.
1622 RunGit(['checkout', '-q', root_branch])
1623
1624 print
1625 print 'Upload complete for dependent branches!'
1626 for dependent_branch in dependents:
1627 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1628 print ' %s : %s' % (dependent_branch, upload_status)
1629 print
1630
1631 return 0
1632
1633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001635 """Show status of changelists.
1636
1637 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001638 - Red not sent for review or broken
1639 - Blue waiting for review
1640 - Yellow waiting for you to reply to review
1641 - Green LGTM'ed
1642 - Magenta in the commit queue
1643 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001644
1645 Also see 'git cl comments'.
1646 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 parser.add_option('--field',
1648 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001649 parser.add_option('-f', '--fast', action='store_true',
1650 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001651 parser.add_option(
1652 '-j', '--maxjobs', action='store', type=int,
1653 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001654
1655 auth.add_auth_options(parser)
1656 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001657 if args:
1658 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001659 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001662 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 if options.field.startswith('desc'):
1664 print cl.GetDescription()
1665 elif options.field == 'id':
1666 issueid = cl.GetIssue()
1667 if issueid:
1668 print issueid
1669 elif options.field == 'patch':
1670 patchset = cl.GetPatchset()
1671 if patchset:
1672 print patchset
1673 elif options.field == 'url':
1674 url = cl.GetIssueURL()
1675 if url:
1676 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001677 return 0
1678
1679 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1680 if not branches:
1681 print('No local branch found.')
1682 return 0
1683
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001684 changes = (
1685 Changelist(branchref=b, auth_config=auth_config)
1686 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001687 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001688 alignment = max(5, max(len(b) for b in branches))
1689 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001690 output = get_cl_statuses(branches,
1691 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001692 max_processes=options.maxjobs,
1693 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001694
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001695 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001696 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001697 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001698 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001699 b, i, status = output.next()
1700 branch_statuses[b] = (i, status)
1701 issue_url, status = branch_statuses.pop(branch)
1702 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001703 reset = Fore.RESET
1704 if not sys.stdout.isatty():
1705 color = ''
1706 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001707 status_str = '(%s)' % status if status else ''
1708 print ' %*s : %s%s %s%s' % (
1709 alignment, ShortBranchName(branch), color, issue_url, status_str,
1710 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001712 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001713 print
1714 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001715 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001716 if not cl.GetIssue():
1717 print 'No issue assigned.'
1718 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001719 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001720 if not options.fast:
1721 print 'Issue description:'
1722 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723 return 0
1724
1725
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001726def colorize_CMDstatus_doc():
1727 """To be called once in main() to add colors to git cl status help."""
1728 colors = [i for i in dir(Fore) if i[0].isupper()]
1729
1730 def colorize_line(line):
1731 for color in colors:
1732 if color in line.upper():
1733 # Extract whitespaces first and the leading '-'.
1734 indent = len(line) - len(line.lstrip(' ')) + 1
1735 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1736 return line
1737
1738 lines = CMDstatus.__doc__.splitlines()
1739 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1740
1741
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001742@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001744 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745
1746 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001747 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001748 parser.add_option('-r', '--reverse', action='store_true',
1749 help='Lookup the branch(es) for the specified issues. If '
1750 'no issues are specified, all branches with mapped '
1751 'issues will be listed.')
1752 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753
dnj@chromium.org406c4402015-03-03 17:22:28 +00001754 if options.reverse:
1755 branches = RunGit(['for-each-ref', 'refs/heads',
1756 '--format=%(refname:short)']).splitlines()
1757
1758 # Reverse issue lookup.
1759 issue_branch_map = {}
1760 for branch in branches:
1761 cl = Changelist(branchref=branch)
1762 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1763 if not args:
1764 args = sorted(issue_branch_map.iterkeys())
1765 for issue in args:
1766 if not issue:
1767 continue
1768 print 'Branch for issue number %s: %s' % (
1769 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1770 else:
1771 cl = Changelist()
1772 if len(args) > 0:
1773 try:
1774 issue = int(args[0])
1775 except ValueError:
1776 DieWithError('Pass a number to set the issue or none to list it.\n'
1777 'Maybe you want to run git cl status?')
1778 cl.SetIssue(issue)
1779 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 return 0
1781
1782
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001783def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001784 """Shows or posts review comments for any changelist."""
1785 parser.add_option('-a', '--add-comment', dest='comment',
1786 help='comment to add to an issue')
1787 parser.add_option('-i', dest='issue',
1788 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001789 parser.add_option('-j', '--json-file',
1790 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001791 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001792 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001793 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001794
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001795 issue = None
1796 if options.issue:
1797 try:
1798 issue = int(options.issue)
1799 except ValueError:
1800 DieWithError('A review issue id is expected to be a number')
1801
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001802 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001803
1804 if options.comment:
1805 cl.AddComment(options.comment)
1806 return 0
1807
1808 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001809 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001810 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001811 summary.append({
1812 'date': message['date'],
1813 'lgtm': False,
1814 'message': message['text'],
1815 'not_lgtm': False,
1816 'sender': message['sender'],
1817 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001818 if message['disapproval']:
1819 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001820 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001821 elif message['approval']:
1822 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001823 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001824 elif message['sender'] == data['owner_email']:
1825 color = Fore.MAGENTA
1826 else:
1827 color = Fore.BLUE
1828 print '\n%s%s %s%s' % (
1829 color, message['date'].split('.', 1)[0], message['sender'],
1830 Fore.RESET)
1831 if message['text'].strip():
1832 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001833 if options.json_file:
1834 with open(options.json_file, 'wb') as f:
1835 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001836 return 0
1837
1838
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001839def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001840 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001841 parser.add_option('-d', '--display', action='store_true',
1842 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001843 auth.add_auth_options(parser)
1844 options, _ = parser.parse_args(args)
1845 auth_config = auth.extract_auth_config_from_options(options)
1846 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001847 if not cl.GetIssue():
1848 DieWithError('This branch has no associated changelist.')
1849 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001850 if options.display:
1851 print description.description
1852 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001853 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001854 if cl.GetDescription() != description.description:
1855 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001856 return 0
1857
1858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859def CreateDescriptionFromLog(args):
1860 """Pulls out the commit log to use as a base for the CL description."""
1861 log_args = []
1862 if len(args) == 1 and not args[0].endswith('.'):
1863 log_args = [args[0] + '..']
1864 elif len(args) == 1 and args[0].endswith('...'):
1865 log_args = [args[0][:-1]]
1866 elif len(args) == 2:
1867 log_args = [args[0] + '..' + args[1]]
1868 else:
1869 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001870 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871
1872
thestig@chromium.org44202a22014-03-11 19:22:18 +00001873def CMDlint(parser, args):
1874 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001875 parser.add_option('--filter', action='append', metavar='-x,+y',
1876 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001877 auth.add_auth_options(parser)
1878 options, args = parser.parse_args(args)
1879 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001880
1881 # Access to a protected member _XX of a client class
1882 # pylint: disable=W0212
1883 try:
1884 import cpplint
1885 import cpplint_chromium
1886 except ImportError:
1887 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1888 return 1
1889
1890 # Change the current working directory before calling lint so that it
1891 # shows the correct base.
1892 previous_cwd = os.getcwd()
1893 os.chdir(settings.GetRoot())
1894 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001895 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001896 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1897 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001898 if not files:
1899 print "Cannot lint an empty CL"
1900 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001901
1902 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001903 command = args + files
1904 if options.filter:
1905 command = ['--filter=' + ','.join(options.filter)] + command
1906 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001907
1908 white_regex = re.compile(settings.GetLintRegex())
1909 black_regex = re.compile(settings.GetLintIgnoreRegex())
1910 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1911 for filename in filenames:
1912 if white_regex.match(filename):
1913 if black_regex.match(filename):
1914 print "Ignoring file %s" % filename
1915 else:
1916 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1917 extra_check_functions)
1918 else:
1919 print "Skipping file %s" % filename
1920 finally:
1921 os.chdir(previous_cwd)
1922 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1923 if cpplint._cpplint_state.error_count != 0:
1924 return 1
1925 return 0
1926
1927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001929 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001930 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001932 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001933 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001934 auth.add_auth_options(parser)
1935 options, args = parser.parse_args(args)
1936 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
sbc@chromium.org71437c02015-04-09 19:29:40 +00001938 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001939 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 return 1
1941
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001942 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 if args:
1944 base_branch = args[0]
1945 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001946 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001947 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001949 cl.RunHook(
1950 committing=not options.upload,
1951 may_prompt=False,
1952 verbose=options.verbose,
1953 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001954 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955
1956
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001957def AddChangeIdToCommitMessage(options, args):
1958 """Re-commits using the current message, assumes the commit hook is in
1959 place.
1960 """
1961 log_desc = options.message or CreateDescriptionFromLog(args)
1962 git_command = ['commit', '--amend', '-m', log_desc]
1963 RunGit(git_command)
1964 new_log_desc = CreateDescriptionFromLog(args)
1965 if CHANGE_ID in new_log_desc:
1966 print 'git-cl: Added Change-Id to commit message.'
1967 else:
1968 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1969
1970
piman@chromium.org336f9122014-09-04 02:16:55 +00001971def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001972 """upload the current branch to gerrit."""
1973 # We assume the remote called "origin" is the one we want.
1974 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001975 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001976
1977 remote, remote_branch = cl.GetRemoteBranch()
1978 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1979 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001981 change_desc = ChangeDescription(
1982 options.message or CreateDescriptionFromLog(args))
1983 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001984 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001986
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001987 if options.squash:
1988 # Try to get the message from a previous upload.
1989 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1990 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1991 if not message:
1992 if not options.force:
1993 change_desc.prompt()
1994
1995 if CHANGE_ID not in change_desc.description:
1996 # Run the commit-msg hook without modifying the head commit by writing
1997 # the commit message to a temporary file and running the hook over it,
1998 # then reading the file back in.
1999 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2000 'commit-msg')
2001 file_handle, msg_file = tempfile.mkstemp(text=True,
2002 prefix='commit_msg')
2003 try:
2004 try:
2005 with os.fdopen(file_handle, 'w') as fileobj:
2006 fileobj.write(change_desc.description)
2007 finally:
2008 os.close(file_handle)
2009 RunCommand([commit_msg_hook, msg_file])
2010 change_desc.set_description(gclient_utils.FileRead(msg_file))
2011 finally:
2012 os.remove(msg_file)
2013
2014 if not change_desc.description:
2015 print "Description is empty; aborting."
2016 return 1
2017
2018 message = change_desc.description
2019
2020 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2021 if remote is '.':
2022 # If our upstream branch is local, we base our squashed commit on its
2023 # squashed version.
2024 parent = ('refs/heads/git_cl_uploads/' +
2025 scm.GIT.ShortBranchName(upstream_branch))
2026
2027 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2028 # will create additional CLs when uploading.
2029 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2030 RunGitSilent(['rev-parse', parent + ':'])):
2031 print 'Upload upstream branch ' + upstream_branch + ' first.'
2032 return 1
2033 else:
2034 parent = cl.GetCommonAncestorWithUpstream()
2035
2036 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2037 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2038 '-m', message]).strip()
2039 else:
2040 if CHANGE_ID not in change_desc.description:
2041 AddChangeIdToCommitMessage(options, args)
2042 ref_to_push = 'HEAD'
2043 parent = '%s/%s' % (gerrit_remote, branch)
2044
2045 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2046 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002047 if len(commits) > 1:
2048 print('WARNING: This will upload %d commits. Run the following command '
2049 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002050 print('git log %s..%s' % (parent, ref_to_push))
2051 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002052 'commit.')
2053 ask_for_data('About to upload; enter to confirm.')
2054
piman@chromium.org336f9122014-09-04 02:16:55 +00002055 if options.reviewers or options.tbr_owners:
2056 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057
ukai@chromium.orge8077812012-02-03 03:41:46 +00002058 receive_options = []
2059 cc = cl.GetCCList().split(',')
2060 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002061 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002062 cc = filter(None, cc)
2063 if cc:
2064 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002065 if change_desc.get_reviewers():
2066 receive_options.extend(
2067 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068
ukai@chromium.orge8077812012-02-03 03:41:46 +00002069 git_command = ['push']
2070 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002071 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002072 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002073 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002074 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002075
2076 if options.squash:
2077 head = RunGit(['rev-parse', 'HEAD']).strip()
2078 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2079
ukai@chromium.orge8077812012-02-03 03:41:46 +00002080 # TODO(ukai): parse Change-Id: and set issue number?
2081 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083
wittman@chromium.org455dc922015-01-26 20:15:50 +00002084def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2085 """Computes the remote branch ref to use for the CL.
2086
2087 Args:
2088 remote (str): The git remote for the CL.
2089 remote_branch (str): The git remote branch for the CL.
2090 target_branch (str): The target branch specified by the user.
2091 pending_prefix (str): The pending prefix from the settings.
2092 """
2093 if not (remote and remote_branch):
2094 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002095
wittman@chromium.org455dc922015-01-26 20:15:50 +00002096 if target_branch:
2097 # Cannonicalize branch references to the equivalent local full symbolic
2098 # refs, which are then translated into the remote full symbolic refs
2099 # below.
2100 if '/' not in target_branch:
2101 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2102 else:
2103 prefix_replacements = (
2104 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2105 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2106 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2107 )
2108 match = None
2109 for regex, replacement in prefix_replacements:
2110 match = re.search(regex, target_branch)
2111 if match:
2112 remote_branch = target_branch.replace(match.group(0), replacement)
2113 break
2114 if not match:
2115 # This is a branch path but not one we recognize; use as-is.
2116 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002117 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2118 # Handle the refs that need to land in different refs.
2119 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002120
wittman@chromium.org455dc922015-01-26 20:15:50 +00002121 # Create the true path to the remote branch.
2122 # Does the following translation:
2123 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2124 # * refs/remotes/origin/master -> refs/heads/master
2125 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2126 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2127 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2128 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2129 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2130 'refs/heads/')
2131 elif remote_branch.startswith('refs/remotes/branch-heads'):
2132 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2133 # If a pending prefix exists then replace refs/ with it.
2134 if pending_prefix:
2135 remote_branch = remote_branch.replace('refs/', pending_prefix)
2136 return remote_branch
2137
2138
piman@chromium.org336f9122014-09-04 02:16:55 +00002139def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002140 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2142 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002143 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144 if options.emulate_svn_auto_props:
2145 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146
2147 change_desc = None
2148
pgervais@chromium.org91141372014-01-09 23:27:20 +00002149 if options.email is not None:
2150 upload_args.extend(['--email', options.email])
2151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002152 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002153 if options.title:
2154 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002155 if options.message:
2156 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002157 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 print ("This branch is associated with issue %s. "
2159 "Adding patch to that issue." % cl.GetIssue())
2160 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002161 if options.title:
2162 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002163 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002164 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002165 if options.reviewers or options.tbr_owners:
2166 change_desc.update_reviewers(options.reviewers,
2167 options.tbr_owners,
2168 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002169 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002170 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002171
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002172 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173 print "Description is empty; aborting."
2174 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002175
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002176 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002177 if change_desc.get_reviewers():
2178 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002179 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002180 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002181 DieWithError("Must specify reviewers to send email.")
2182 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002183
2184 # We check this before applying rietveld.private assuming that in
2185 # rietveld.cc only addresses which we can send private CLs to are listed
2186 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2187 # --private is specified explicitly on the command line.
2188 if options.private:
2189 logging.warn('rietveld.cc is ignored since private flag is specified. '
2190 'You need to review and add them manually if necessary.')
2191 cc = cl.GetCCListWithoutDefault()
2192 else:
2193 cc = cl.GetCCList()
2194 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002195 if cc:
2196 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002197
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002198 if options.private or settings.GetDefaultPrivateFlag() == "True":
2199 upload_args.append('--private')
2200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002201 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002202 if not options.find_copies:
2203 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002204
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205 # Include the upstream repo's URL in the change -- this is useful for
2206 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002207 remote_url = cl.GetGitBaseUrlFromConfig()
2208 if not remote_url:
2209 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002210 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002211 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002212 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2213 remote_url = (cl.GetRemoteUrl() + '@'
2214 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215 if remote_url:
2216 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002217 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002218 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2219 settings.GetPendingRefPrefix())
2220 if target_ref:
2221 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002222
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002223 # Look for dependent patchsets. See crbug.com/480453 for more details.
2224 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2225 upstream_branch = ShortBranchName(upstream_branch)
2226 if remote is '.':
2227 # A local branch is being tracked.
2228 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002229 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002230 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002231 print ('Skipping dependency patchset upload because git config '
2232 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002233 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002234 else:
2235 auth_config = auth.extract_auth_config_from_options(options)
2236 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2237 branch_cl_issue_url = branch_cl.GetIssueURL()
2238 branch_cl_issue = branch_cl.GetIssue()
2239 branch_cl_patchset = branch_cl.GetPatchset()
2240 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2241 upload_args.extend(
2242 ['--depends_on_patchset', '%s:%s' % (
2243 branch_cl_issue, branch_cl_patchset)])
2244 print
2245 print ('The current branch (%s) is tracking a local branch (%s) with '
2246 'an associated CL.') % (cl.GetBranch(), local_branch)
2247 print 'Adding %s/#ps%s as a dependency patchset.' % (
2248 branch_cl_issue_url, branch_cl_patchset)
2249 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002250
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002251 project = settings.GetProject()
2252 if project:
2253 upload_args.extend(['--project', project])
2254
rmistry@google.comef966222015-04-07 11:15:01 +00002255 if options.cq_dry_run:
2256 upload_args.extend(['--cq_dry_run'])
2257
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002258 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002259 upload_args = ['upload'] + upload_args + args
2260 logging.info('upload.RealMain(%s)', upload_args)
2261 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002262 issue = int(issue)
2263 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002264 except KeyboardInterrupt:
2265 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002266 except:
2267 # If we got an exception after the user typed a description for their
2268 # change, back up the description before re-raising.
2269 if change_desc:
2270 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2271 print '\nGot exception while uploading -- saving description to %s\n' \
2272 % backup_path
2273 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002274 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002275 backup_file.close()
2276 raise
2277
2278 if not cl.GetIssue():
2279 cl.SetIssue(issue)
2280 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002281
2282 if options.use_commit_queue:
2283 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284 return 0
2285
2286
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002287def cleanup_list(l):
2288 """Fixes a list so that comma separated items are put as individual items.
2289
2290 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2291 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2292 """
2293 items = sum((i.split(',') for i in l), [])
2294 stripped_items = (i.strip() for i in items)
2295 return sorted(filter(None, stripped_items))
2296
2297
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002298@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002299def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002300 """Uploads the current changelist to codereview.
2301
2302 Can skip dependency patchset uploads for a branch by running:
2303 git config branch.branch_name.skip-deps-uploads True
2304 To unset run:
2305 git config --unset branch.branch_name.skip-deps-uploads
2306 Can also set the above globally by using the --global flag.
2307 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002308 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2309 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002310 parser.add_option('--bypass-watchlists', action='store_true',
2311 dest='bypass_watchlists',
2312 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002313 parser.add_option('-f', action='store_true', dest='force',
2314 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002315 parser.add_option('-m', dest='message', help='message for patchset')
2316 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002317 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002318 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002319 help='reviewer email addresses')
2320 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002321 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002322 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002323 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002324 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002325 parser.add_option('--emulate_svn_auto_props',
2326 '--emulate-svn-auto-props',
2327 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002328 dest="emulate_svn_auto_props",
2329 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002330 parser.add_option('-c', '--use-commit-queue', action='store_true',
2331 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002332 parser.add_option('--private', action='store_true',
2333 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002334 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002335 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002336 metavar='TARGET',
2337 help='Apply CL to remote ref TARGET. ' +
2338 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002339 parser.add_option('--squash', action='store_true',
2340 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002341 parser.add_option('--email', default=None,
2342 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002343 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2344 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002345 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2346 help='Send the patchset to do a CQ dry run right after '
2347 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002348 parser.add_option('--dependencies', action='store_true',
2349 help='Uploads CLs of all the local branches that depend on '
2350 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002351
rmistry@google.com2dd99862015-06-22 12:22:18 +00002352 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002353 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002354 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002355 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002356 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002357
sbc@chromium.org71437c02015-04-09 19:29:40 +00002358 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002359 return 1
2360
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002361 options.reviewers = cleanup_list(options.reviewers)
2362 options.cc = cleanup_list(options.cc)
2363
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002364 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002365 if args:
2366 # TODO(ukai): is it ok for gerrit case?
2367 base_branch = args[0]
2368 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002369 if cl.GetBranch() is None:
2370 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2371
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002372 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002373 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002374 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002375
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002376 # Make sure authenticated to Rietveld before running expensive hooks. It is
2377 # a fast, best efforts check. Rietveld still can reject the authentication
2378 # during the actual upload.
2379 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2380 authenticator = auth.get_authenticator_for_host(
2381 cl.GetRietveldServer(), auth_config)
2382 if not authenticator.has_cached_credentials():
2383 raise auth.LoginRequiredError(cl.GetRietveldServer())
2384
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002385 # Apply watchlists on upload.
2386 change = cl.GetChange(base_branch, None)
2387 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2388 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002389 if not options.bypass_watchlists:
2390 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002391
ukai@chromium.orge8077812012-02-03 03:41:46 +00002392 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002393 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002394 # Set the reviewer list now so that presubmit checks can access it.
2395 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002396 change_description.update_reviewers(options.reviewers,
2397 options.tbr_owners,
2398 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002399 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002400 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002401 may_prompt=not options.force,
2402 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002403 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002404 if not hook_results.should_continue():
2405 return 1
2406 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002407 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002408
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002409 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002410 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002411 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002412 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002413 print ('The last upload made from this repository was patchset #%d but '
2414 'the most recent patchset on the server is #%d.'
2415 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002416 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2417 'from another machine or branch the patch you\'re uploading now '
2418 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002419 ask_for_data('About to upload; enter to confirm.')
2420
iannucci@chromium.org79540052012-10-19 23:15:26 +00002421 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002422 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002423 return GerritUpload(options, args, cl, change)
2424 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002425 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002426 git_set_branch_value('last-upload-hash',
2427 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002428 # Run post upload hooks, if specified.
2429 if settings.GetRunPostUploadHook():
2430 presubmit_support.DoPostUploadExecuter(
2431 change,
2432 cl,
2433 settings.GetRoot(),
2434 options.verbose,
2435 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002436
rmistry@google.com2dd99862015-06-22 12:22:18 +00002437 # Upload all dependencies if specified.
2438 if options.dependencies:
2439 print
2440 print '--dependencies has been specified.'
2441 print 'All dependent local branches will be re-uploaded.'
2442 print
2443 # Remove the dependencies flag from args so that we do not end up in a
2444 # loop.
2445 orig_args.remove('--dependencies')
2446 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002447 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002448
2449
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002450def IsSubmoduleMergeCommit(ref):
2451 # When submodules are added to the repo, we expect there to be a single
2452 # non-git-svn merge commit at remote HEAD with a signature comment.
2453 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002454 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002455 return RunGit(cmd) != ''
2456
2457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002458def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002459 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002460
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002461 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002462 Updates changelog with metadata (e.g. pointer to review).
2463 Pushes/dcommits the code upstream.
2464 Updates review and closes.
2465 """
2466 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2467 help='bypass upload presubmit hook')
2468 parser.add_option('-m', dest='message',
2469 help="override review description")
2470 parser.add_option('-f', action='store_true', dest='force',
2471 help="force yes to questions (don't prompt)")
2472 parser.add_option('-c', dest='contributor',
2473 help="external contributor for patch (appended to " +
2474 "description and used as author for git). Should be " +
2475 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002476 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002477 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002479 auth_config = auth.extract_auth_config_from_options(options)
2480
2481 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002482
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002483 current = cl.GetBranch()
2484 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2485 if not settings.GetIsGitSvn() and remote == '.':
2486 print
2487 print 'Attempting to push branch %r into another local branch!' % current
2488 print
2489 print 'Either reparent this branch on top of origin/master:'
2490 print ' git reparent-branch --root'
2491 print
2492 print 'OR run `git rebase-update` if you think the parent branch is already'
2493 print 'committed.'
2494 print
2495 print ' Current parent: %r' % upstream_branch
2496 return 1
2497
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002498 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002499 # Default to merging against our best guess of the upstream branch.
2500 args = [cl.GetUpstreamBranch()]
2501
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002502 if options.contributor:
2503 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2504 print "Please provide contibutor as 'First Last <email@example.com>'"
2505 return 1
2506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002508 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509
sbc@chromium.org71437c02015-04-09 19:29:40 +00002510 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002511 return 1
2512
2513 # This rev-list syntax means "show all commits not in my branch that
2514 # are in base_branch".
2515 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2516 base_branch]).splitlines()
2517 if upstream_commits:
2518 print ('Base branch "%s" has %d commits '
2519 'not in this branch.' % (base_branch, len(upstream_commits)))
2520 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2521 return 1
2522
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002523 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002524 svn_head = None
2525 if cmd == 'dcommit' or base_has_submodules:
2526 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2527 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002528
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002529 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002530 # If the base_head is a submodule merge commit, the first parent of the
2531 # base_head should be a git-svn commit, which is what we're interested in.
2532 base_svn_head = base_branch
2533 if base_has_submodules:
2534 base_svn_head += '^1'
2535
2536 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537 if extra_commits:
2538 print ('This branch has %d additional commits not upstreamed yet.'
2539 % len(extra_commits.splitlines()))
2540 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2541 'before attempting to %s.' % (base_branch, cmd))
2542 return 1
2543
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002544 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002545 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002546 author = None
2547 if options.contributor:
2548 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002549 hook_results = cl.RunHook(
2550 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002551 may_prompt=not options.force,
2552 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002553 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002554 if not hook_results.should_continue():
2555 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002556
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002557 # Check the tree status if the tree status URL is set.
2558 status = GetTreeStatus()
2559 if 'closed' == status:
2560 print('The tree is closed. Please wait for it to reopen. Use '
2561 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2562 return 1
2563 elif 'unknown' == status:
2564 print('Unable to determine tree status. Please verify manually and '
2565 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2566 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002567 else:
2568 breakpad.SendStack(
2569 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002570 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2571 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002572 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002573
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002574 change_desc = ChangeDescription(options.message)
2575 if not change_desc.description and cl.GetIssue():
2576 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002577
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002578 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002579 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002580 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002581 else:
2582 print 'No description set.'
2583 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2584 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586 # Keep a separate copy for the commit message, because the commit message
2587 # contains the link to the Rietveld issue, while the Rietveld message contains
2588 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002589 # Keep a separate copy for the commit message.
2590 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002591 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002592
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002593 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002594 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002595 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002596 # after it. Add a period on a new line to circumvent this. Also add a space
2597 # before the period to make sure that Gitiles continues to correctly resolve
2598 # the URL.
2599 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002600 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002601 commit_desc.append_footer('Patch from %s.' % options.contributor)
2602
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002603 print('Description:')
2604 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002605
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002606 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002607 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002608 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002609
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002610 # We want to squash all this branch's commits into one commit with the proper
2611 # description. We do this by doing a "reset --soft" to the base branch (which
2612 # keeps the working copy the same), then dcommitting that. If origin/master
2613 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2614 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002615 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002616 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2617 # Delete the branches if they exist.
2618 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2619 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2620 result = RunGitWithCode(showref_cmd)
2621 if result[0] == 0:
2622 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002623
2624 # We might be in a directory that's present in this branch but not in the
2625 # trunk. Move up to the top of the tree so that git commands that expect a
2626 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002627 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628 if rel_base_path:
2629 os.chdir(rel_base_path)
2630
2631 # Stuff our change into the merge branch.
2632 # We wrap in a try...finally block so if anything goes wrong,
2633 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002634 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002635 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002636 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002637 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002639 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002640 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002641 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002642 RunGit(
2643 [
2644 'commit', '--author', options.contributor,
2645 '-m', commit_desc.description,
2646 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002648 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002649 if base_has_submodules:
2650 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2651 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2652 RunGit(['checkout', CHERRY_PICK_BRANCH])
2653 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002654 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002655 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002656 pending_prefix = settings.GetPendingRefPrefix()
2657 if not pending_prefix or branch.startswith(pending_prefix):
2658 # If not using refs/pending/heads/* at all, or target ref is already set
2659 # to pending, then push to the target ref directly.
2660 retcode, output = RunGitWithCode(
2661 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002662 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002663 else:
2664 # Cherry-pick the change on top of pending ref and then push it.
2665 assert branch.startswith('refs/'), branch
2666 assert pending_prefix[-1] == '/', pending_prefix
2667 pending_ref = pending_prefix + branch[len('refs/'):]
2668 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002669 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002670 if retcode == 0:
2671 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672 else:
2673 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002674 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002675 'svn', 'dcommit',
2676 '-C%s' % options.similarity,
2677 '--no-rebase', '--rmdir',
2678 ]
2679 if settings.GetForceHttpsCommitUrl():
2680 # Allow forcing https commit URLs for some projects that don't allow
2681 # committing to http URLs (like Google Code).
2682 remote_url = cl.GetGitSvnRemoteUrl()
2683 if urlparse.urlparse(remote_url).scheme == 'http':
2684 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002685 cmd_args.append('--commit-url=%s' % remote_url)
2686 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002687 if 'Committed r' in output:
2688 revision = re.match(
2689 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2690 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691 finally:
2692 # And then swap back to the original branch and clean up.
2693 RunGit(['checkout', '-q', cl.GetBranch()])
2694 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002695 if base_has_submodules:
2696 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002698 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002699 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002700 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002701
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002702 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002703 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002704 try:
2705 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2706 # We set pushed_to_pending to False, since it made it all the way to the
2707 # real ref.
2708 pushed_to_pending = False
2709 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002710 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002713 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002715 if not to_pending:
2716 if viewvc_url and revision:
2717 change_desc.append_footer(
2718 'Committed: %s%s' % (viewvc_url, revision))
2719 elif revision:
2720 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721 print ('Closing issue '
2722 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002723 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002725 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002726 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002727 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002728 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002729 if options.bypass_hooks:
2730 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2731 else:
2732 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002733 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002734 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002735
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002736 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002737 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2738 print 'The commit is in the pending queue (%s).' % pending_ref
2739 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002740 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002741 'footer.' % branch)
2742
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002743 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2744 if os.path.isfile(hook):
2745 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002746
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002747 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748
2749
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002750def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2751 print
2752 print 'Waiting for commit to be landed on %s...' % real_ref
2753 print '(If you are impatient, you may Ctrl-C once without harm)'
2754 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2755 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2756
2757 loop = 0
2758 while True:
2759 sys.stdout.write('fetching (%d)... \r' % loop)
2760 sys.stdout.flush()
2761 loop += 1
2762
2763 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2764 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2765 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2766 for commit in commits.splitlines():
2767 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2768 print 'Found commit on %s' % real_ref
2769 return commit
2770
2771 current_rev = to_rev
2772
2773
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002774def PushToGitPending(remote, pending_ref, upstream_ref):
2775 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2776
2777 Returns:
2778 (retcode of last operation, output log of last operation).
2779 """
2780 assert pending_ref.startswith('refs/'), pending_ref
2781 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2782 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2783 code = 0
2784 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002785 max_attempts = 3
2786 attempts_left = max_attempts
2787 while attempts_left:
2788 if attempts_left != max_attempts:
2789 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2790 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002791
2792 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002793 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002794 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002795 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002796 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002797 print 'Fetch failed with exit code %d.' % code
2798 if out.strip():
2799 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002800 continue
2801
2802 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002803 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002804 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002805 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002806 if code:
2807 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002808 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2809 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002810 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2811 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002812 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002813 return code, out
2814
2815 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002816 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002817 code, out = RunGitWithCode(
2818 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2819 if code == 0:
2820 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002821 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002822 return code, out
2823
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002824 print 'Push failed with exit code %d.' % code
2825 if out.strip():
2826 print out.strip()
2827 if IsFatalPushFailure(out):
2828 print (
2829 'Fatal push error. Make sure your .netrc credentials and git '
2830 'user.email are correct and you have push access to the repo.')
2831 return code, out
2832
2833 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002834 return code, out
2835
2836
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002837def IsFatalPushFailure(push_stdout):
2838 """True if retrying push won't help."""
2839 return '(prohibited by Gerrit)' in push_stdout
2840
2841
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002842@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002843def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002844 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002846 if get_footer_svn_id():
2847 # If it looks like previous commits were mirrored with git-svn.
2848 message = """This repository appears to be a git-svn mirror, but no
2849upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2850 else:
2851 message = """This doesn't appear to be an SVN repository.
2852If your project has a true, writeable git repository, you probably want to run
2853'git cl land' instead.
2854If your project has a git mirror of an upstream SVN master, you probably need
2855to run 'git svn init'.
2856
2857Using the wrong command might cause your commit to appear to succeed, and the
2858review to be closed, without actually landing upstream. If you choose to
2859proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002860 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002861 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 return SendUpstream(parser, args, 'dcommit')
2863
2864
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002865@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002866def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002867 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002868 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002869 print('This appears to be an SVN repository.')
2870 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002871 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002872 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002873 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874
2875
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002876@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002878 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002879 parser.add_option('-b', dest='newbranch',
2880 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002881 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002883 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2884 help='Change to the directory DIR immediately, '
2885 'before doing anything else.')
2886 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002887 help='failed patches spew .rej files rather than '
2888 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2890 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002891 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002892 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002893 auth_config = auth.extract_auth_config_from_options(options)
2894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 if len(args) != 1:
2896 parser.print_help()
2897 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002898 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002900 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002901 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002902 return 1
2903
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002904 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002905 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002906
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002907 if options.newbranch:
2908 if options.force:
2909 RunGit(['branch', '-D', options.newbranch],
2910 stderr=subprocess2.PIPE, error_ok=True)
2911 RunGit(['checkout', '-b', options.newbranch,
2912 Changelist().GetUpstreamBranch()])
2913
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002914 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002915 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002916
2917
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002918def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002919 # PatchIssue should never be called with a dirty tree. It is up to the
2920 # caller to check this, but just in case we assert here since the
2921 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002922 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002923
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002924 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002925 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002926 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002927 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002928 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002929 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002931 # Assume it's a URL to the patch. Default to https.
2932 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002933 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002934 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935 DieWithError('Must pass an issue ID or full URL for '
2936 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002937 issue = int(match.group(2))
2938 cl = Changelist(issue=issue, auth_config=auth_config)
2939 cl.rietveld_server = match.group(1)
2940 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002941 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943 # Switch up to the top-level directory, if necessary, in preparation for
2944 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002945 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 if top:
2947 os.chdir(top)
2948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 # Git patches have a/ at the beginning of source paths. We strip that out
2950 # with a sed script rather than the -p flag to patch so we can feed either
2951 # Git or svn-style patches into the same apply command.
2952 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002953 try:
2954 patch_data = subprocess2.check_output(
2955 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2956 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 DieWithError('Git patch mungling failed.')
2958 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960 # We use "git apply" to apply the patch instead of "patch" so that we can
2961 # pick up file adds.
2962 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002963 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002964 if directory:
2965 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002966 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002967 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002968 elif IsGitVersionAtLeast('1.7.12'):
2969 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002970 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002971 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002972 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002973 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002974 print 'Failed to apply the patch'
2975 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976
2977 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002978 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002979 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2980 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002981 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2982 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002983 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002984 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002985 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002986 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002987 else:
2988 print "Patch applied to index."
2989 return 0
2990
2991
2992def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002993 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002994 # Provide a wrapper for git svn rebase to help avoid accidental
2995 # git svn dcommit.
2996 # It's the only command that doesn't use parser at all since we just defer
2997 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002998
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002999 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003000
3001
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003002def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003003 """Fetches the tree status and returns either 'open', 'closed',
3004 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003005 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006 if url:
3007 status = urllib2.urlopen(url).read().lower()
3008 if status.find('closed') != -1 or status == '0':
3009 return 'closed'
3010 elif status.find('open') != -1 or status == '1':
3011 return 'open'
3012 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003013 return 'unset'
3014
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003016def GetTreeStatusReason():
3017 """Fetches the tree status from a json url and returns the message
3018 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003019 url = settings.GetTreeStatusUrl()
3020 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003021 connection = urllib2.urlopen(json_url)
3022 status = json.loads(connection.read())
3023 connection.close()
3024 return status['message']
3025
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003026
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003027def GetBuilderMaster(bot_list):
3028 """For a given builder, fetch the master from AE if available."""
3029 map_url = 'https://builders-map.appspot.com/'
3030 try:
3031 master_map = json.load(urllib2.urlopen(map_url))
3032 except urllib2.URLError as e:
3033 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3034 (map_url, e))
3035 except ValueError as e:
3036 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3037 if not master_map:
3038 return None, 'Failed to build master map.'
3039
3040 result_master = ''
3041 for bot in bot_list:
3042 builder = bot.split(':', 1)[0]
3043 master_list = master_map.get(builder, [])
3044 if not master_list:
3045 return None, ('No matching master for builder %s.' % builder)
3046 elif len(master_list) > 1:
3047 return None, ('The builder name %s exists in multiple masters %s.' %
3048 (builder, master_list))
3049 else:
3050 cur_master = master_list[0]
3051 if not result_master:
3052 result_master = cur_master
3053 elif result_master != cur_master:
3054 return None, 'The builders do not belong to the same master.'
3055 return result_master, None
3056
3057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003058def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003059 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003060 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003061 status = GetTreeStatus()
3062 if 'unset' == status:
3063 print 'You must configure your tree status URL by running "git cl config".'
3064 return 2
3065
3066 print "The tree is %s" % status
3067 print
3068 print GetTreeStatusReason()
3069 if status != 'open':
3070 return 1
3071 return 0
3072
3073
maruel@chromium.org15192402012-09-06 12:38:29 +00003074def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003075 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003076 group = optparse.OptionGroup(parser, "Try job options")
3077 group.add_option(
3078 "-b", "--bot", action="append",
3079 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3080 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003081 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003082 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003083 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003084 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003085 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003086 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003087 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003088 "-r", "--revision",
3089 help="Revision to use for the try job; default: the "
3090 "revision will be determined by the try server; see "
3091 "its waterfall for more info")
3092 group.add_option(
3093 "-c", "--clobber", action="store_true", default=False,
3094 help="Force a clobber before building; e.g. don't do an "
3095 "incremental build")
3096 group.add_option(
3097 "--project",
3098 help="Override which project to use. Projects are defined "
3099 "server-side to define what default bot set to use")
3100 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003101 "-p", "--property", dest="properties", action="append", default=[],
3102 help="Specify generic properties in the form -p key1=value1 -p "
3103 "key2=value2 etc (buildbucket only). The value will be treated as "
3104 "json if decodable, or as string otherwise.")
3105 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003106 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003107 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003108 "--use-rietveld", action="store_true", default=False,
3109 help="Use Rietveld to trigger try jobs.")
3110 group.add_option(
3111 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3112 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003113 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003115 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003116 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003117
machenbach@chromium.org45453142015-09-15 08:45:22 +00003118 if options.use_rietveld and options.properties:
3119 parser.error('Properties can only be specified with buildbucket')
3120
3121 # Make sure that all properties are prop=value pairs.
3122 bad_params = [x for x in options.properties if '=' not in x]
3123 if bad_params:
3124 parser.error('Got properties with missing "=": %s' % bad_params)
3125
maruel@chromium.org15192402012-09-06 12:38:29 +00003126 if args:
3127 parser.error('Unknown arguments: %s' % args)
3128
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003129 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003130 if not cl.GetIssue():
3131 parser.error('Need to upload first')
3132
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003133 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003134 if props.get('closed'):
3135 parser.error('Cannot send tryjobs for a closed CL')
3136
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003137 if props.get('private'):
3138 parser.error('Cannot use trybots with private issue')
3139
maruel@chromium.org15192402012-09-06 12:38:29 +00003140 if not options.name:
3141 options.name = cl.GetBranch()
3142
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003143 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003144 options.master, err_msg = GetBuilderMaster(options.bot)
3145 if err_msg:
3146 parser.error('Tryserver master cannot be found because: %s\n'
3147 'Please manually specify the tryserver master'
3148 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003149
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003150 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003151 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003152 if not options.bot:
3153 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003154
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003155 # Get try masters from PRESUBMIT.py files.
3156 masters = presubmit_support.DoGetTryMasters(
3157 change,
3158 change.LocalPaths(),
3159 settings.GetRoot(),
3160 None,
3161 None,
3162 options.verbose,
3163 sys.stdout)
3164 if masters:
3165 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003166
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003167 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3168 options.bot = presubmit_support.DoGetTrySlaves(
3169 change,
3170 change.LocalPaths(),
3171 settings.GetRoot(),
3172 None,
3173 None,
3174 options.verbose,
3175 sys.stdout)
3176 if not options.bot:
3177 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003178
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003179 builders_and_tests = {}
3180 # TODO(machenbach): The old style command-line options don't support
3181 # multiple try masters yet.
3182 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3183 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3184
3185 for bot in old_style:
3186 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003187 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003188 elif ',' in bot:
3189 parser.error('Specify one bot per --bot flag')
3190 else:
3191 builders_and_tests.setdefault(bot, []).append('defaulttests')
3192
3193 for bot, tests in new_style:
3194 builders_and_tests.setdefault(bot, []).extend(tests)
3195
3196 # Return a master map with one master to be backwards compatible. The
3197 # master name defaults to an empty string, which will cause the master
3198 # not to be set on rietveld (deprecated).
3199 return {options.master: builders_and_tests}
3200
3201 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003202
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003203 for builders in masters.itervalues():
3204 if any('triggered' in b for b in builders):
3205 print >> sys.stderr, (
3206 'ERROR You are trying to send a job to a triggered bot. This type of'
3207 ' bot requires an\ninitial job from a parent (usually a builder). '
3208 'Instead send your job to the parent.\n'
3209 'Bot list: %s' % builders)
3210 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003211
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003212 patchset = cl.GetMostRecentPatchset()
3213 if patchset and patchset != cl.GetPatchset():
3214 print(
3215 '\nWARNING Mismatch between local config and server. Did a previous '
3216 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3217 'Continuing using\npatchset %s.\n' % patchset)
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003218 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003219 try:
3220 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3221 except BuildbucketResponseException as ex:
3222 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003223 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003224 except Exception as e:
3225 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3226 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3227 e, stacktrace)
3228 return 1
3229 else:
3230 try:
3231 cl.RpcServer().trigger_distributed_try_jobs(
3232 cl.GetIssue(), patchset, options.name, options.clobber,
3233 options.revision, masters)
3234 except urllib2.HTTPError as e:
3235 if e.code == 404:
3236 print('404 from rietveld; '
3237 'did you mean to use "git try" instead of "git cl try"?')
3238 return 1
3239 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003240
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003241 for (master, builders) in sorted(masters.iteritems()):
3242 if master:
3243 print 'Master: %s' % master
3244 length = max(len(builder) for builder in builders)
3245 for builder in sorted(builders):
3246 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003247 return 0
3248
3249
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003250@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003252 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003253 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003254 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003255 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003258 if args:
3259 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003260 branch = cl.GetBranch()
3261 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003262 cl = Changelist()
3263 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003264
3265 # Clear configured merge-base, if there is one.
3266 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003267 else:
3268 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269 return 0
3270
3271
thestig@chromium.org00858c82013-12-02 23:08:03 +00003272def CMDweb(parser, args):
3273 """Opens the current CL in the web browser."""
3274 _, args = parser.parse_args(args)
3275 if args:
3276 parser.error('Unrecognized args: %s' % ' '.join(args))
3277
3278 issue_url = Changelist().GetIssueURL()
3279 if not issue_url:
3280 print >> sys.stderr, 'ERROR No issue to open'
3281 return 1
3282
3283 webbrowser.open(issue_url)
3284 return 0
3285
3286
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003287def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003288 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003289 auth.add_auth_options(parser)
3290 options, args = parser.parse_args(args)
3291 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003292 if args:
3293 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003294 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003295 props = cl.GetIssueProperties()
3296 if props.get('private'):
3297 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003298 cl.SetFlag('commit', '1')
3299 return 0
3300
3301
groby@chromium.org411034a2013-02-26 15:12:01 +00003302def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003303 """Closes the issue."""
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)
groby@chromium.org411034a2013-02-26 15:12:01 +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)
groby@chromium.org411034a2013-02-26 15:12:01 +00003310 # Ensure there actually is an issue to close.
3311 cl.GetDescription()
3312 cl.CloseIssue()
3313 return 0
3314
3315
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003316def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003317 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003318 auth.add_auth_options(parser)
3319 options, args = parser.parse_args(args)
3320 auth_config = auth.extract_auth_config_from_options(options)
3321 if args:
3322 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003323
3324 # Uncommitted (staged and unstaged) changes will be destroyed by
3325 # "git reset --hard" if there are merging conflicts in PatchIssue().
3326 # Staged changes would be committed along with the patch from last
3327 # upload, hence counted toward the "last upload" side in the final
3328 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003329 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003330 return 1
3331
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003332 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003333 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003334 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003335 if not issue:
3336 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003337 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003338 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003339
3340 # Create a new branch based on the merge-base
3341 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3342 try:
3343 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003344 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003345 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003346 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003347 return rtn
3348
wychen@chromium.org06928532015-02-03 02:11:29 +00003349 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003350 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003351 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003352 finally:
3353 RunGit(['checkout', '-q', branch])
3354 RunGit(['branch', '-D', TMP_BRANCH])
3355
3356 return 0
3357
3358
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003359def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003360 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003361 parser.add_option(
3362 '--no-color',
3363 action='store_true',
3364 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003365 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003366 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003367 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003368
3369 author = RunGit(['config', 'user.email']).strip() or None
3370
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003371 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003372
3373 if args:
3374 if len(args) > 1:
3375 parser.error('Unknown args')
3376 base_branch = args[0]
3377 else:
3378 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003379 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003380
3381 change = cl.GetChange(base_branch, None)
3382 return owners_finder.OwnersFinder(
3383 [f.LocalPath() for f in
3384 cl.GetChange(base_branch, None).AffectedFiles()],
3385 change.RepositoryRoot(), author,
3386 fopen=file, os_path=os.path, glob=glob.glob,
3387 disable_color=options.no_color).run()
3388
3389
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003390def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3391 """Generates a diff command."""
3392 # Generate diff for the current branch's changes.
3393 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3394 upstream_commit, '--' ]
3395
3396 if args:
3397 for arg in args:
3398 if os.path.isdir(arg):
3399 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3400 elif os.path.isfile(arg):
3401 diff_cmd.append(arg)
3402 else:
3403 DieWithError('Argument "%s" is not a file or a directory' % arg)
3404 else:
3405 diff_cmd.extend('*' + ext for ext in extensions)
3406
3407 return diff_cmd
3408
3409
enne@chromium.org555cfe42014-01-29 18:21:39 +00003410@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003411def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003412 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003413 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003414 parser.add_option('--full', action='store_true',
3415 help='Reformat the full content of all touched files')
3416 parser.add_option('--dry-run', action='store_true',
3417 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003418 parser.add_option('--python', action='store_true',
3419 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003420 parser.add_option('--diff', action='store_true',
3421 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003422 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003423
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003424 # git diff generates paths against the root of the repository. Change
3425 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003426 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003427 if rel_base_path:
3428 os.chdir(rel_base_path)
3429
digit@chromium.org29e47272013-05-17 17:01:46 +00003430 # Grab the merge-base commit, i.e. the upstream commit of the current
3431 # branch when it was created or the last time it was rebased. This is
3432 # to cover the case where the user may have called "git fetch origin",
3433 # moving the origin branch to a newer commit, but hasn't rebased yet.
3434 upstream_commit = None
3435 cl = Changelist()
3436 upstream_branch = cl.GetUpstreamBranch()
3437 if upstream_branch:
3438 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3439 upstream_commit = upstream_commit.strip()
3440
3441 if not upstream_commit:
3442 DieWithError('Could not find base commit for this branch. '
3443 'Are you in detached state?')
3444
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003445 if opts.full:
3446 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003447 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003448 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003449 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003450 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003451
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003452 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003453 diff_output = RunGit(diff_cmd)
3454
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003455 top_dir = os.path.normpath(
3456 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3457
3458 # Locate the clang-format binary in the checkout
3459 try:
3460 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3461 except clang_format.NotFoundError, e:
3462 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003463
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003464 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3465 # formatted. This is used to block during the presubmit.
3466 return_value = 0
3467
digit@chromium.org29e47272013-05-17 17:01:46 +00003468 if opts.full:
3469 # diff_output is a list of files to send to clang-format.
3470 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003471 if files:
3472 cmd = [clang_format_tool]
3473 if not opts.dry_run and not opts.diff:
3474 cmd.append('-i')
3475 stdout = RunCommand(cmd + files, cwd=top_dir)
3476 if opts.diff:
3477 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003478 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003479 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003480 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003481 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003482 try:
3483 script = clang_format.FindClangFormatScriptInChromiumTree(
3484 'clang-format-diff.py')
3485 except clang_format.NotFoundError, e:
3486 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003487
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003488 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003489 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003490 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003491
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003492 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003493 if opts.diff:
3494 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003495 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003496 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003497
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003498 # Similar code to above, but using yapf on .py files rather than clang-format
3499 # on C/C++ files
3500 if opts.python:
3501 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3502 diff_output = RunGit(diff_cmd)
3503 yapf_tool = gclient_utils.FindExecutable('yapf')
3504 if yapf_tool is None:
3505 DieWithError('yapf not found in PATH')
3506
3507 if opts.full:
3508 files = diff_output.splitlines()
3509 if files:
3510 cmd = [yapf_tool]
3511 if not opts.dry_run and not opts.diff:
3512 cmd.append('-i')
3513 stdout = RunCommand(cmd + files, cwd=top_dir)
3514 if opts.diff:
3515 sys.stdout.write(stdout)
3516 else:
3517 # TODO(sbc): yapf --lines mode still has some issues.
3518 # https://github.com/google/yapf/issues/154
3519 DieWithError('--python currently only works with --full')
3520
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003521 # Build a diff command that only operates on dart files. dart's formatter
3522 # does not have the nice property of only operating on modified chunks, so
3523 # hard code full.
3524 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3525 args, ['.dart'])
3526 dart_diff_output = RunGit(dart_diff_cmd)
3527 if dart_diff_output:
3528 try:
3529 command = [dart_format.FindDartFmtToolInChromiumTree()]
3530 if not opts.dry_run and not opts.diff:
3531 command.append('-w')
3532 command.extend(dart_diff_output.splitlines())
3533
3534 stdout = RunCommand(command, cwd=top_dir, env=env)
3535 if opts.dry_run and stdout:
3536 return_value = 2
3537 except dart_format.NotFoundError as e:
3538 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3539 'this checkout.')
3540
3541 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003542
3543
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003544@subcommand.usage('<codereview url or issue id>')
3545def CMDcheckout(parser, args):
3546 """Checks out a branch associated with a given Rietveld issue."""
3547 _, args = parser.parse_args(args)
3548
3549 if len(args) != 1:
3550 parser.print_help()
3551 return 1
3552
3553 if re.match(r'\d+', args[0]):
3554 target_issue = args[0]
3555 elif args[0].startswith('http'):
3556 target_issue = re.sub(r'.*/(\d+)/?', r'\1', args[0])
3557 else:
3558 parser.print_help()
3559 return 1
3560
3561 key_and_issues = [x.split() for x in RunGit(
3562 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3563 .splitlines()]
3564 branches = []
3565 for key, issue in key_and_issues:
3566 if issue == target_issue:
3567 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3568
3569 if len(branches) == 0:
3570 print 'No branch found for issue %s.' % target_issue
3571 return 1
3572 if len(branches) == 1:
3573 RunGit(['checkout', branches[0]])
3574 else:
3575 print 'Multiple branches match issue %s:' % target_issue
3576 for i in range(len(branches)):
3577 print '%d: %s' % (i, branches[i])
3578 which = raw_input('Choose by index: ')
3579 try:
3580 RunGit(['checkout', branches[int(which)]])
3581 except (IndexError, ValueError):
3582 print 'Invalid selection, not checking out any branch.'
3583 return 1
3584
3585 return 0
3586
3587
maruel@chromium.org29404b52014-09-08 22:58:00 +00003588def CMDlol(parser, args):
3589 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003590 print zlib.decompress(base64.b64decode(
3591 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3592 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3593 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3594 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003595 return 0
3596
3597
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003598class OptionParser(optparse.OptionParser):
3599 """Creates the option parse and add --verbose support."""
3600 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003601 optparse.OptionParser.__init__(
3602 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003603 self.add_option(
3604 '-v', '--verbose', action='count', default=0,
3605 help='Use 2 times for more debugging info')
3606
3607 def parse_args(self, args=None, values=None):
3608 options, args = optparse.OptionParser.parse_args(self, args, values)
3609 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3610 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3611 return options, args
3612
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003613
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003614def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003615 if sys.hexversion < 0x02060000:
3616 print >> sys.stderr, (
3617 '\nYour python version %s is unsupported, please upgrade.\n' %
3618 sys.version.split(' ', 1)[0])
3619 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003620
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003621 # Reload settings.
3622 global settings
3623 settings = Settings()
3624
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003625 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003626 dispatcher = subcommand.CommandDispatcher(__name__)
3627 try:
3628 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003629 except auth.AuthenticationError as e:
3630 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003631 except urllib2.HTTPError, e:
3632 if e.code != 500:
3633 raise
3634 DieWithError(
3635 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3636 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003637 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638
3639
3640if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003641 # These affect sys.stdout so do it outside of main() to simplify mocks in
3642 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003643 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003644 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003645 try:
3646 sys.exit(main(sys.argv[1:]))
3647 except KeyboardInterrupt:
3648 sys.stderr.write('interrupted\n')
3649 sys.exit(1)