blob: 9fcc3b75269b676f0455d4be5ae672a97a682334 [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,
270 'changes': [
271 {'author': {'email': issue_props['owner_email']}},
272 ],
273 'properties': {
274 'category': category,
275 'issue': issue,
276 'master': master,
277 'patch_project': issue_props['project'],
278 'patch_storage': 'rietveld',
279 'patchset': patchset,
280 'reason': options.name,
281 'revision': options.revision,
282 '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)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001789 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001790 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001791 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001792
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001793 issue = None
1794 if options.issue:
1795 try:
1796 issue = int(options.issue)
1797 except ValueError:
1798 DieWithError('A review issue id is expected to be a number')
1799
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001800 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001801
1802 if options.comment:
1803 cl.AddComment(options.comment)
1804 return 0
1805
1806 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001807 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001808 if message['disapproval']:
1809 color = Fore.RED
1810 elif message['approval']:
1811 color = Fore.GREEN
1812 elif message['sender'] == data['owner_email']:
1813 color = Fore.MAGENTA
1814 else:
1815 color = Fore.BLUE
1816 print '\n%s%s %s%s' % (
1817 color, message['date'].split('.', 1)[0], message['sender'],
1818 Fore.RESET)
1819 if message['text'].strip():
1820 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001821 return 0
1822
1823
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001824def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001825 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001826 parser.add_option('-d', '--display', action='store_true',
1827 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001828 auth.add_auth_options(parser)
1829 options, _ = parser.parse_args(args)
1830 auth_config = auth.extract_auth_config_from_options(options)
1831 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001832 if not cl.GetIssue():
1833 DieWithError('This branch has no associated changelist.')
1834 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001835 if options.display:
1836 print description.description
1837 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001838 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001839 if cl.GetDescription() != description.description:
1840 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001841 return 0
1842
1843
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844def CreateDescriptionFromLog(args):
1845 """Pulls out the commit log to use as a base for the CL description."""
1846 log_args = []
1847 if len(args) == 1 and not args[0].endswith('.'):
1848 log_args = [args[0] + '..']
1849 elif len(args) == 1 and args[0].endswith('...'):
1850 log_args = [args[0][:-1]]
1851 elif len(args) == 2:
1852 log_args = [args[0] + '..' + args[1]]
1853 else:
1854 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001855 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001856
1857
thestig@chromium.org44202a22014-03-11 19:22:18 +00001858def CMDlint(parser, args):
1859 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001860 parser.add_option('--filter', action='append', metavar='-x,+y',
1861 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001862 auth.add_auth_options(parser)
1863 options, args = parser.parse_args(args)
1864 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001865
1866 # Access to a protected member _XX of a client class
1867 # pylint: disable=W0212
1868 try:
1869 import cpplint
1870 import cpplint_chromium
1871 except ImportError:
1872 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1873 return 1
1874
1875 # Change the current working directory before calling lint so that it
1876 # shows the correct base.
1877 previous_cwd = os.getcwd()
1878 os.chdir(settings.GetRoot())
1879 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001880 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001881 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1882 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001883 if not files:
1884 print "Cannot lint an empty CL"
1885 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001886
1887 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001888 command = args + files
1889 if options.filter:
1890 command = ['--filter=' + ','.join(options.filter)] + command
1891 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001892
1893 white_regex = re.compile(settings.GetLintRegex())
1894 black_regex = re.compile(settings.GetLintIgnoreRegex())
1895 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1896 for filename in filenames:
1897 if white_regex.match(filename):
1898 if black_regex.match(filename):
1899 print "Ignoring file %s" % filename
1900 else:
1901 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1902 extra_check_functions)
1903 else:
1904 print "Skipping file %s" % filename
1905 finally:
1906 os.chdir(previous_cwd)
1907 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1908 if cpplint._cpplint_state.error_count != 0:
1909 return 1
1910 return 0
1911
1912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001914 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001915 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001917 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001918 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001919 auth.add_auth_options(parser)
1920 options, args = parser.parse_args(args)
1921 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922
sbc@chromium.org71437c02015-04-09 19:29:40 +00001923 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001924 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925 return 1
1926
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001927 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 if args:
1929 base_branch = args[0]
1930 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001931 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001932 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001934 cl.RunHook(
1935 committing=not options.upload,
1936 may_prompt=False,
1937 verbose=options.verbose,
1938 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001939 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940
1941
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001942def AddChangeIdToCommitMessage(options, args):
1943 """Re-commits using the current message, assumes the commit hook is in
1944 place.
1945 """
1946 log_desc = options.message or CreateDescriptionFromLog(args)
1947 git_command = ['commit', '--amend', '-m', log_desc]
1948 RunGit(git_command)
1949 new_log_desc = CreateDescriptionFromLog(args)
1950 if CHANGE_ID in new_log_desc:
1951 print 'git-cl: Added Change-Id to commit message.'
1952 else:
1953 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1954
1955
piman@chromium.org336f9122014-09-04 02:16:55 +00001956def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001957 """upload the current branch to gerrit."""
1958 # We assume the remote called "origin" is the one we want.
1959 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001960 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001961
1962 remote, remote_branch = cl.GetRemoteBranch()
1963 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1964 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001965
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001966 change_desc = ChangeDescription(
1967 options.message or CreateDescriptionFromLog(args))
1968 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001969 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001971
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001972 if options.squash:
1973 # Try to get the message from a previous upload.
1974 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1975 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1976 if not message:
1977 if not options.force:
1978 change_desc.prompt()
1979
1980 if CHANGE_ID not in change_desc.description:
1981 # Run the commit-msg hook without modifying the head commit by writing
1982 # the commit message to a temporary file and running the hook over it,
1983 # then reading the file back in.
1984 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1985 'commit-msg')
1986 file_handle, msg_file = tempfile.mkstemp(text=True,
1987 prefix='commit_msg')
1988 try:
1989 try:
1990 with os.fdopen(file_handle, 'w') as fileobj:
1991 fileobj.write(change_desc.description)
1992 finally:
1993 os.close(file_handle)
1994 RunCommand([commit_msg_hook, msg_file])
1995 change_desc.set_description(gclient_utils.FileRead(msg_file))
1996 finally:
1997 os.remove(msg_file)
1998
1999 if not change_desc.description:
2000 print "Description is empty; aborting."
2001 return 1
2002
2003 message = change_desc.description
2004
2005 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2006 if remote is '.':
2007 # If our upstream branch is local, we base our squashed commit on its
2008 # squashed version.
2009 parent = ('refs/heads/git_cl_uploads/' +
2010 scm.GIT.ShortBranchName(upstream_branch))
2011
2012 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2013 # will create additional CLs when uploading.
2014 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2015 RunGitSilent(['rev-parse', parent + ':'])):
2016 print 'Upload upstream branch ' + upstream_branch + ' first.'
2017 return 1
2018 else:
2019 parent = cl.GetCommonAncestorWithUpstream()
2020
2021 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2022 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2023 '-m', message]).strip()
2024 else:
2025 if CHANGE_ID not in change_desc.description:
2026 AddChangeIdToCommitMessage(options, args)
2027 ref_to_push = 'HEAD'
2028 parent = '%s/%s' % (gerrit_remote, branch)
2029
2030 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2031 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002032 if len(commits) > 1:
2033 print('WARNING: This will upload %d commits. Run the following command '
2034 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002035 print('git log %s..%s' % (parent, ref_to_push))
2036 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002037 'commit.')
2038 ask_for_data('About to upload; enter to confirm.')
2039
piman@chromium.org336f9122014-09-04 02:16:55 +00002040 if options.reviewers or options.tbr_owners:
2041 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042
ukai@chromium.orge8077812012-02-03 03:41:46 +00002043 receive_options = []
2044 cc = cl.GetCCList().split(',')
2045 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002046 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002047 cc = filter(None, cc)
2048 if cc:
2049 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002050 if change_desc.get_reviewers():
2051 receive_options.extend(
2052 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053
ukai@chromium.orge8077812012-02-03 03:41:46 +00002054 git_command = ['push']
2055 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002056 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002057 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002058 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002059 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002060
2061 if options.squash:
2062 head = RunGit(['rev-parse', 'HEAD']).strip()
2063 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2064
ukai@chromium.orge8077812012-02-03 03:41:46 +00002065 # TODO(ukai): parse Change-Id: and set issue number?
2066 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002067
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068
wittman@chromium.org455dc922015-01-26 20:15:50 +00002069def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2070 """Computes the remote branch ref to use for the CL.
2071
2072 Args:
2073 remote (str): The git remote for the CL.
2074 remote_branch (str): The git remote branch for the CL.
2075 target_branch (str): The target branch specified by the user.
2076 pending_prefix (str): The pending prefix from the settings.
2077 """
2078 if not (remote and remote_branch):
2079 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002080
wittman@chromium.org455dc922015-01-26 20:15:50 +00002081 if target_branch:
2082 # Cannonicalize branch references to the equivalent local full symbolic
2083 # refs, which are then translated into the remote full symbolic refs
2084 # below.
2085 if '/' not in target_branch:
2086 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2087 else:
2088 prefix_replacements = (
2089 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2090 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2091 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2092 )
2093 match = None
2094 for regex, replacement in prefix_replacements:
2095 match = re.search(regex, target_branch)
2096 if match:
2097 remote_branch = target_branch.replace(match.group(0), replacement)
2098 break
2099 if not match:
2100 # This is a branch path but not one we recognize; use as-is.
2101 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002102 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2103 # Handle the refs that need to land in different refs.
2104 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002105
wittman@chromium.org455dc922015-01-26 20:15:50 +00002106 # Create the true path to the remote branch.
2107 # Does the following translation:
2108 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2109 # * refs/remotes/origin/master -> refs/heads/master
2110 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2111 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2112 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2113 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2114 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2115 'refs/heads/')
2116 elif remote_branch.startswith('refs/remotes/branch-heads'):
2117 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2118 # If a pending prefix exists then replace refs/ with it.
2119 if pending_prefix:
2120 remote_branch = remote_branch.replace('refs/', pending_prefix)
2121 return remote_branch
2122
2123
piman@chromium.org336f9122014-09-04 02:16:55 +00002124def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002125 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2127 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002128 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 if options.emulate_svn_auto_props:
2130 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131
2132 change_desc = None
2133
pgervais@chromium.org91141372014-01-09 23:27:20 +00002134 if options.email is not None:
2135 upload_args.extend(['--email', options.email])
2136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002137 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002138 if options.title:
2139 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002140 if options.message:
2141 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002142 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 print ("This branch is associated with issue %s. "
2144 "Adding patch to that issue." % cl.GetIssue())
2145 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002146 if options.title:
2147 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002148 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002149 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002150 if options.reviewers or options.tbr_owners:
2151 change_desc.update_reviewers(options.reviewers,
2152 options.tbr_owners,
2153 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002154 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002155 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002156
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002157 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158 print "Description is empty; aborting."
2159 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002160
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002161 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002162 if change_desc.get_reviewers():
2163 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002164 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002165 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002166 DieWithError("Must specify reviewers to send email.")
2167 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002168
2169 # We check this before applying rietveld.private assuming that in
2170 # rietveld.cc only addresses which we can send private CLs to are listed
2171 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2172 # --private is specified explicitly on the command line.
2173 if options.private:
2174 logging.warn('rietveld.cc is ignored since private flag is specified. '
2175 'You need to review and add them manually if necessary.')
2176 cc = cl.GetCCListWithoutDefault()
2177 else:
2178 cc = cl.GetCCList()
2179 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002180 if cc:
2181 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002182
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002183 if options.private or settings.GetDefaultPrivateFlag() == "True":
2184 upload_args.append('--private')
2185
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002186 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002187 if not options.find_copies:
2188 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 # Include the upstream repo's URL in the change -- this is useful for
2191 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002192 remote_url = cl.GetGitBaseUrlFromConfig()
2193 if not remote_url:
2194 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002195 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002196 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002197 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2198 remote_url = (cl.GetRemoteUrl() + '@'
2199 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 if remote_url:
2201 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002202 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002203 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2204 settings.GetPendingRefPrefix())
2205 if target_ref:
2206 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002207
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002208 # Look for dependent patchsets. See crbug.com/480453 for more details.
2209 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2210 upstream_branch = ShortBranchName(upstream_branch)
2211 if remote is '.':
2212 # A local branch is being tracked.
2213 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002214 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002215 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002216 print ('Skipping dependency patchset upload because git config '
2217 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002218 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002219 else:
2220 auth_config = auth.extract_auth_config_from_options(options)
2221 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2222 branch_cl_issue_url = branch_cl.GetIssueURL()
2223 branch_cl_issue = branch_cl.GetIssue()
2224 branch_cl_patchset = branch_cl.GetPatchset()
2225 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2226 upload_args.extend(
2227 ['--depends_on_patchset', '%s:%s' % (
2228 branch_cl_issue, branch_cl_patchset)])
2229 print
2230 print ('The current branch (%s) is tracking a local branch (%s) with '
2231 'an associated CL.') % (cl.GetBranch(), local_branch)
2232 print 'Adding %s/#ps%s as a dependency patchset.' % (
2233 branch_cl_issue_url, branch_cl_patchset)
2234 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002235
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002236 project = settings.GetProject()
2237 if project:
2238 upload_args.extend(['--project', project])
2239
rmistry@google.comef966222015-04-07 11:15:01 +00002240 if options.cq_dry_run:
2241 upload_args.extend(['--cq_dry_run'])
2242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002243 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002244 upload_args = ['upload'] + upload_args + args
2245 logging.info('upload.RealMain(%s)', upload_args)
2246 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002247 issue = int(issue)
2248 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002249 except KeyboardInterrupt:
2250 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002251 except:
2252 # If we got an exception after the user typed a description for their
2253 # change, back up the description before re-raising.
2254 if change_desc:
2255 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2256 print '\nGot exception while uploading -- saving description to %s\n' \
2257 % backup_path
2258 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002259 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260 backup_file.close()
2261 raise
2262
2263 if not cl.GetIssue():
2264 cl.SetIssue(issue)
2265 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002266
2267 if options.use_commit_queue:
2268 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002269 return 0
2270
2271
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002272def cleanup_list(l):
2273 """Fixes a list so that comma separated items are put as individual items.
2274
2275 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2276 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2277 """
2278 items = sum((i.split(',') for i in l), [])
2279 stripped_items = (i.strip() for i in items)
2280 return sorted(filter(None, stripped_items))
2281
2282
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002283@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002284def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002285 """Uploads the current changelist to codereview.
2286
2287 Can skip dependency patchset uploads for a branch by running:
2288 git config branch.branch_name.skip-deps-uploads True
2289 To unset run:
2290 git config --unset branch.branch_name.skip-deps-uploads
2291 Can also set the above globally by using the --global flag.
2292 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002293 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2294 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002295 parser.add_option('--bypass-watchlists', action='store_true',
2296 dest='bypass_watchlists',
2297 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002298 parser.add_option('-f', action='store_true', dest='force',
2299 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002300 parser.add_option('-m', dest='message', help='message for patchset')
2301 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002302 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002303 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002304 help='reviewer email addresses')
2305 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002306 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002307 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002308 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002309 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002310 parser.add_option('--emulate_svn_auto_props',
2311 '--emulate-svn-auto-props',
2312 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002313 dest="emulate_svn_auto_props",
2314 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002315 parser.add_option('-c', '--use-commit-queue', action='store_true',
2316 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002317 parser.add_option('--private', action='store_true',
2318 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002319 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002320 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002321 metavar='TARGET',
2322 help='Apply CL to remote ref TARGET. ' +
2323 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002324 parser.add_option('--squash', action='store_true',
2325 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002326 parser.add_option('--email', default=None,
2327 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002328 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2329 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002330 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2331 help='Send the patchset to do a CQ dry run right after '
2332 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002333 parser.add_option('--dependencies', action='store_true',
2334 help='Uploads CLs of all the local branches that depend on '
2335 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002336
rmistry@google.com2dd99862015-06-22 12:22:18 +00002337 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002338 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002339 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002340 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002341 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002342
sbc@chromium.org71437c02015-04-09 19:29:40 +00002343 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002344 return 1
2345
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002346 options.reviewers = cleanup_list(options.reviewers)
2347 options.cc = cleanup_list(options.cc)
2348
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002349 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002350 if args:
2351 # TODO(ukai): is it ok for gerrit case?
2352 base_branch = args[0]
2353 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002354 if cl.GetBranch() is None:
2355 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2356
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002357 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002358 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002359 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002360
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002361 # Make sure authenticated to Rietveld before running expensive hooks. It is
2362 # a fast, best efforts check. Rietveld still can reject the authentication
2363 # during the actual upload.
2364 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2365 authenticator = auth.get_authenticator_for_host(
2366 cl.GetRietveldServer(), auth_config)
2367 if not authenticator.has_cached_credentials():
2368 raise auth.LoginRequiredError(cl.GetRietveldServer())
2369
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002370 # Apply watchlists on upload.
2371 change = cl.GetChange(base_branch, None)
2372 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2373 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002374 if not options.bypass_watchlists:
2375 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002376
ukai@chromium.orge8077812012-02-03 03:41:46 +00002377 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002378 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002379 # Set the reviewer list now so that presubmit checks can access it.
2380 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002381 change_description.update_reviewers(options.reviewers,
2382 options.tbr_owners,
2383 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002384 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002385 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002386 may_prompt=not options.force,
2387 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002388 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002389 if not hook_results.should_continue():
2390 return 1
2391 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002392 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002393
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002394 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002395 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002396 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002397 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002398 print ('The last upload made from this repository was patchset #%d but '
2399 'the most recent patchset on the server is #%d.'
2400 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002401 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2402 'from another machine or branch the patch you\'re uploading now '
2403 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002404 ask_for_data('About to upload; enter to confirm.')
2405
iannucci@chromium.org79540052012-10-19 23:15:26 +00002406 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002407 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002408 return GerritUpload(options, args, cl, change)
2409 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002410 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002411 git_set_branch_value('last-upload-hash',
2412 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002413 # Run post upload hooks, if specified.
2414 if settings.GetRunPostUploadHook():
2415 presubmit_support.DoPostUploadExecuter(
2416 change,
2417 cl,
2418 settings.GetRoot(),
2419 options.verbose,
2420 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002421
rmistry@google.com2dd99862015-06-22 12:22:18 +00002422 # Upload all dependencies if specified.
2423 if options.dependencies:
2424 print
2425 print '--dependencies has been specified.'
2426 print 'All dependent local branches will be re-uploaded.'
2427 print
2428 # Remove the dependencies flag from args so that we do not end up in a
2429 # loop.
2430 orig_args.remove('--dependencies')
2431 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002432 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002433
2434
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002435def IsSubmoduleMergeCommit(ref):
2436 # When submodules are added to the repo, we expect there to be a single
2437 # non-git-svn merge commit at remote HEAD with a signature comment.
2438 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002439 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002440 return RunGit(cmd) != ''
2441
2442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002443def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002444 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002446 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002447 Updates changelog with metadata (e.g. pointer to review).
2448 Pushes/dcommits the code upstream.
2449 Updates review and closes.
2450 """
2451 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2452 help='bypass upload presubmit hook')
2453 parser.add_option('-m', dest='message',
2454 help="override review description")
2455 parser.add_option('-f', action='store_true', dest='force',
2456 help="force yes to questions (don't prompt)")
2457 parser.add_option('-c', dest='contributor',
2458 help="external contributor for patch (appended to " +
2459 "description and used as author for git). Should be " +
2460 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002461 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002462 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002463 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002464 auth_config = auth.extract_auth_config_from_options(options)
2465
2466 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002468 current = cl.GetBranch()
2469 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2470 if not settings.GetIsGitSvn() and remote == '.':
2471 print
2472 print 'Attempting to push branch %r into another local branch!' % current
2473 print
2474 print 'Either reparent this branch on top of origin/master:'
2475 print ' git reparent-branch --root'
2476 print
2477 print 'OR run `git rebase-update` if you think the parent branch is already'
2478 print 'committed.'
2479 print
2480 print ' Current parent: %r' % upstream_branch
2481 return 1
2482
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002483 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002484 # Default to merging against our best guess of the upstream branch.
2485 args = [cl.GetUpstreamBranch()]
2486
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002487 if options.contributor:
2488 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2489 print "Please provide contibutor as 'First Last <email@example.com>'"
2490 return 1
2491
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002493 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494
sbc@chromium.org71437c02015-04-09 19:29:40 +00002495 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002496 return 1
2497
2498 # This rev-list syntax means "show all commits not in my branch that
2499 # are in base_branch".
2500 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2501 base_branch]).splitlines()
2502 if upstream_commits:
2503 print ('Base branch "%s" has %d commits '
2504 'not in this branch.' % (base_branch, len(upstream_commits)))
2505 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2506 return 1
2507
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002508 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002509 svn_head = None
2510 if cmd == 'dcommit' or base_has_submodules:
2511 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2512 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002513
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002515 # If the base_head is a submodule merge commit, the first parent of the
2516 # base_head should be a git-svn commit, which is what we're interested in.
2517 base_svn_head = base_branch
2518 if base_has_submodules:
2519 base_svn_head += '^1'
2520
2521 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002522 if extra_commits:
2523 print ('This branch has %d additional commits not upstreamed yet.'
2524 % len(extra_commits.splitlines()))
2525 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2526 'before attempting to %s.' % (base_branch, cmd))
2527 return 1
2528
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002529 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002530 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002531 author = None
2532 if options.contributor:
2533 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002534 hook_results = cl.RunHook(
2535 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002536 may_prompt=not options.force,
2537 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002538 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002539 if not hook_results.should_continue():
2540 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002541
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002542 # Check the tree status if the tree status URL is set.
2543 status = GetTreeStatus()
2544 if 'closed' == status:
2545 print('The tree is closed. Please wait for it to reopen. Use '
2546 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2547 return 1
2548 elif 'unknown' == status:
2549 print('Unable to determine tree status. Please verify manually and '
2550 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2551 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002552 else:
2553 breakpad.SendStack(
2554 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002555 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2556 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002557 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002558
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002559 change_desc = ChangeDescription(options.message)
2560 if not change_desc.description and cl.GetIssue():
2561 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002563 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002564 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002565 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002566 else:
2567 print 'No description set.'
2568 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2569 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002570
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002571 # Keep a separate copy for the commit message, because the commit message
2572 # contains the link to the Rietveld issue, while the Rietveld message contains
2573 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002574 # Keep a separate copy for the commit message.
2575 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002576 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002577
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002578 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002579 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002580 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002581 # after it. Add a period on a new line to circumvent this. Also add a space
2582 # before the period to make sure that Gitiles continues to correctly resolve
2583 # the URL.
2584 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586 commit_desc.append_footer('Patch from %s.' % options.contributor)
2587
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002588 print('Description:')
2589 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002591 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002593 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002594
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002595 # We want to squash all this branch's commits into one commit with the proper
2596 # description. We do this by doing a "reset --soft" to the base branch (which
2597 # keeps the working copy the same), then dcommitting that. If origin/master
2598 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2599 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002600 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002601 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2602 # Delete the branches if they exist.
2603 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2604 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2605 result = RunGitWithCode(showref_cmd)
2606 if result[0] == 0:
2607 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002608
2609 # We might be in a directory that's present in this branch but not in the
2610 # trunk. Move up to the top of the tree so that git commands that expect a
2611 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002612 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613 if rel_base_path:
2614 os.chdir(rel_base_path)
2615
2616 # Stuff our change into the merge branch.
2617 # We wrap in a try...finally block so if anything goes wrong,
2618 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002619 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002620 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002621 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002622 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002623 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002624 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002625 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002626 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002627 RunGit(
2628 [
2629 'commit', '--author', options.contributor,
2630 '-m', commit_desc.description,
2631 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002632 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002634 if base_has_submodules:
2635 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2636 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2637 RunGit(['checkout', CHERRY_PICK_BRANCH])
2638 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002639 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002640 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002641 pending_prefix = settings.GetPendingRefPrefix()
2642 if not pending_prefix or branch.startswith(pending_prefix):
2643 # If not using refs/pending/heads/* at all, or target ref is already set
2644 # to pending, then push to the target ref directly.
2645 retcode, output = RunGitWithCode(
2646 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002647 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002648 else:
2649 # Cherry-pick the change on top of pending ref and then push it.
2650 assert branch.startswith('refs/'), branch
2651 assert pending_prefix[-1] == '/', pending_prefix
2652 pending_ref = pending_prefix + branch[len('refs/'):]
2653 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002654 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002655 if retcode == 0:
2656 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657 else:
2658 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002659 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002660 'svn', 'dcommit',
2661 '-C%s' % options.similarity,
2662 '--no-rebase', '--rmdir',
2663 ]
2664 if settings.GetForceHttpsCommitUrl():
2665 # Allow forcing https commit URLs for some projects that don't allow
2666 # committing to http URLs (like Google Code).
2667 remote_url = cl.GetGitSvnRemoteUrl()
2668 if urlparse.urlparse(remote_url).scheme == 'http':
2669 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002670 cmd_args.append('--commit-url=%s' % remote_url)
2671 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002672 if 'Committed r' in output:
2673 revision = re.match(
2674 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2675 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676 finally:
2677 # And then swap back to the original branch and clean up.
2678 RunGit(['checkout', '-q', cl.GetBranch()])
2679 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002680 if base_has_submodules:
2681 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002683 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002684 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002685 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002686
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002687 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002688 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002689 try:
2690 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2691 # We set pushed_to_pending to False, since it made it all the way to the
2692 # real ref.
2693 pushed_to_pending = False
2694 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002695 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002698 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002699 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002700 if not to_pending:
2701 if viewvc_url and revision:
2702 change_desc.append_footer(
2703 'Committed: %s%s' % (viewvc_url, revision))
2704 elif revision:
2705 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002706 print ('Closing issue '
2707 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002708 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002710 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002711 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002712 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002713 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002714 if options.bypass_hooks:
2715 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2716 else:
2717 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002718 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002719 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002720
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002721 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002722 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2723 print 'The commit is in the pending queue (%s).' % pending_ref
2724 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002725 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002726 'footer.' % branch)
2727
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002728 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2729 if os.path.isfile(hook):
2730 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002731
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002732 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733
2734
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002735def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2736 print
2737 print 'Waiting for commit to be landed on %s...' % real_ref
2738 print '(If you are impatient, you may Ctrl-C once without harm)'
2739 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2740 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2741
2742 loop = 0
2743 while True:
2744 sys.stdout.write('fetching (%d)... \r' % loop)
2745 sys.stdout.flush()
2746 loop += 1
2747
2748 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2749 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2750 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2751 for commit in commits.splitlines():
2752 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2753 print 'Found commit on %s' % real_ref
2754 return commit
2755
2756 current_rev = to_rev
2757
2758
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002759def PushToGitPending(remote, pending_ref, upstream_ref):
2760 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2761
2762 Returns:
2763 (retcode of last operation, output log of last operation).
2764 """
2765 assert pending_ref.startswith('refs/'), pending_ref
2766 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2767 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2768 code = 0
2769 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002770 max_attempts = 3
2771 attempts_left = max_attempts
2772 while attempts_left:
2773 if attempts_left != max_attempts:
2774 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2775 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002776
2777 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002778 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002779 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002780 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002781 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002782 print 'Fetch failed with exit code %d.' % code
2783 if out.strip():
2784 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002785 continue
2786
2787 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002788 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002789 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002790 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002791 if code:
2792 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002793 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2794 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002795 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2796 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002797 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002798 return code, out
2799
2800 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002801 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002802 code, out = RunGitWithCode(
2803 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2804 if code == 0:
2805 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002806 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002807 return code, out
2808
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002809 print 'Push failed with exit code %d.' % code
2810 if out.strip():
2811 print out.strip()
2812 if IsFatalPushFailure(out):
2813 print (
2814 'Fatal push error. Make sure your .netrc credentials and git '
2815 'user.email are correct and you have push access to the repo.')
2816 return code, out
2817
2818 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002819 return code, out
2820
2821
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002822def IsFatalPushFailure(push_stdout):
2823 """True if retrying push won't help."""
2824 return '(prohibited by Gerrit)' in push_stdout
2825
2826
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002827@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002828def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002829 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002831 if get_footer_svn_id():
2832 # If it looks like previous commits were mirrored with git-svn.
2833 message = """This repository appears to be a git-svn mirror, but no
2834upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2835 else:
2836 message = """This doesn't appear to be an SVN repository.
2837If your project has a true, writeable git repository, you probably want to run
2838'git cl land' instead.
2839If your project has a git mirror of an upstream SVN master, you probably need
2840to run 'git svn init'.
2841
2842Using the wrong command might cause your commit to appear to succeed, and the
2843review to be closed, without actually landing upstream. If you choose to
2844proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002845 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002846 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 return SendUpstream(parser, args, 'dcommit')
2848
2849
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002850@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002851def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002852 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002853 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002854 print('This appears to be an SVN repository.')
2855 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002856 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002857 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002858 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859
2860
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002861@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002863 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 parser.add_option('-b', dest='newbranch',
2865 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002866 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002868 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2869 help='Change to the directory DIR immediately, '
2870 'before doing anything else.')
2871 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002872 help='failed patches spew .rej files rather than '
2873 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2875 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002876 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002878 auth_config = auth.extract_auth_config_from_options(options)
2879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880 if len(args) != 1:
2881 parser.print_help()
2882 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002883 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002884
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002885 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002886 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002887 return 1
2888
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002889 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002890 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002891
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002892 if options.newbranch:
2893 if options.force:
2894 RunGit(['branch', '-D', options.newbranch],
2895 stderr=subprocess2.PIPE, error_ok=True)
2896 RunGit(['checkout', '-b', options.newbranch,
2897 Changelist().GetUpstreamBranch()])
2898
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002899 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002900 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002901
2902
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002903def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002904 # PatchIssue should never be called with a dirty tree. It is up to the
2905 # caller to check this, but just in case we assert here since the
2906 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002907 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002908
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002909 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002911 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002912 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002913 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002914 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002915 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002916 # Assume it's a URL to the patch. Default to https.
2917 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002918 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002919 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920 DieWithError('Must pass an issue ID or full URL for '
2921 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002922 issue = int(match.group(2))
2923 cl = Changelist(issue=issue, auth_config=auth_config)
2924 cl.rietveld_server = match.group(1)
2925 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002926 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002928 # Switch up to the top-level directory, if necessary, in preparation for
2929 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002930 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002931 if top:
2932 os.chdir(top)
2933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934 # Git patches have a/ at the beginning of source paths. We strip that out
2935 # with a sed script rather than the -p flag to patch so we can feed either
2936 # Git or svn-style patches into the same apply command.
2937 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002938 try:
2939 patch_data = subprocess2.check_output(
2940 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2941 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942 DieWithError('Git patch mungling failed.')
2943 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945 # We use "git apply" to apply the patch instead of "patch" so that we can
2946 # pick up file adds.
2947 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002948 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002949 if directory:
2950 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002951 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002952 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002953 elif IsGitVersionAtLeast('1.7.12'):
2954 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002955 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002956 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002957 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002958 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002959 print 'Failed to apply the patch'
2960 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961
2962 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002963 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002964 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2965 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002966 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2967 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002968 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002970 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002971 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 else:
2973 print "Patch applied to index."
2974 return 0
2975
2976
2977def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002978 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002979 # Provide a wrapper for git svn rebase to help avoid accidental
2980 # git svn dcommit.
2981 # It's the only command that doesn't use parser at all since we just defer
2982 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002983
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002984 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002985
2986
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002987def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988 """Fetches the tree status and returns either 'open', 'closed',
2989 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002990 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991 if url:
2992 status = urllib2.urlopen(url).read().lower()
2993 if status.find('closed') != -1 or status == '0':
2994 return 'closed'
2995 elif status.find('open') != -1 or status == '1':
2996 return 'open'
2997 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002998 return 'unset'
2999
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003001def GetTreeStatusReason():
3002 """Fetches the tree status from a json url and returns the message
3003 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003004 url = settings.GetTreeStatusUrl()
3005 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006 connection = urllib2.urlopen(json_url)
3007 status = json.loads(connection.read())
3008 connection.close()
3009 return status['message']
3010
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003011
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003012def GetBuilderMaster(bot_list):
3013 """For a given builder, fetch the master from AE if available."""
3014 map_url = 'https://builders-map.appspot.com/'
3015 try:
3016 master_map = json.load(urllib2.urlopen(map_url))
3017 except urllib2.URLError as e:
3018 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3019 (map_url, e))
3020 except ValueError as e:
3021 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3022 if not master_map:
3023 return None, 'Failed to build master map.'
3024
3025 result_master = ''
3026 for bot in bot_list:
3027 builder = bot.split(':', 1)[0]
3028 master_list = master_map.get(builder, [])
3029 if not master_list:
3030 return None, ('No matching master for builder %s.' % builder)
3031 elif len(master_list) > 1:
3032 return None, ('The builder name %s exists in multiple masters %s.' %
3033 (builder, master_list))
3034 else:
3035 cur_master = master_list[0]
3036 if not result_master:
3037 result_master = cur_master
3038 elif result_master != cur_master:
3039 return None, 'The builders do not belong to the same master.'
3040 return result_master, None
3041
3042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003043def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003044 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003045 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046 status = GetTreeStatus()
3047 if 'unset' == status:
3048 print 'You must configure your tree status URL by running "git cl config".'
3049 return 2
3050
3051 print "The tree is %s" % status
3052 print
3053 print GetTreeStatusReason()
3054 if status != 'open':
3055 return 1
3056 return 0
3057
3058
maruel@chromium.org15192402012-09-06 12:38:29 +00003059def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003060 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003061 group = optparse.OptionGroup(parser, "Try job options")
3062 group.add_option(
3063 "-b", "--bot", action="append",
3064 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3065 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003066 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003067 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003068 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003069 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003070 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003071 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003072 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003073 "-r", "--revision",
3074 help="Revision to use for the try job; default: the "
3075 "revision will be determined by the try server; see "
3076 "its waterfall for more info")
3077 group.add_option(
3078 "-c", "--clobber", action="store_true", default=False,
3079 help="Force a clobber before building; e.g. don't do an "
3080 "incremental build")
3081 group.add_option(
3082 "--project",
3083 help="Override which project to use. Projects are defined "
3084 "server-side to define what default bot set to use")
3085 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003086 "-p", "--property", dest="properties", action="append", default=[],
3087 help="Specify generic properties in the form -p key1=value1 -p "
3088 "key2=value2 etc (buildbucket only). The value will be treated as "
3089 "json if decodable, or as string otherwise.")
3090 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003091 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003092 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003093 "--use-rietveld", action="store_true", default=False,
3094 help="Use Rietveld to trigger try jobs.")
3095 group.add_option(
3096 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3097 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003098 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003099 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003100 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003101 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003102
machenbach@chromium.org45453142015-09-15 08:45:22 +00003103 if options.use_rietveld and options.properties:
3104 parser.error('Properties can only be specified with buildbucket')
3105
3106 # Make sure that all properties are prop=value pairs.
3107 bad_params = [x for x in options.properties if '=' not in x]
3108 if bad_params:
3109 parser.error('Got properties with missing "=": %s' % bad_params)
3110
maruel@chromium.org15192402012-09-06 12:38:29 +00003111 if args:
3112 parser.error('Unknown arguments: %s' % args)
3113
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003115 if not cl.GetIssue():
3116 parser.error('Need to upload first')
3117
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003118 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003119 if props.get('closed'):
3120 parser.error('Cannot send tryjobs for a closed CL')
3121
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003122 if props.get('private'):
3123 parser.error('Cannot use trybots with private issue')
3124
maruel@chromium.org15192402012-09-06 12:38:29 +00003125 if not options.name:
3126 options.name = cl.GetBranch()
3127
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003128 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003129 options.master, err_msg = GetBuilderMaster(options.bot)
3130 if err_msg:
3131 parser.error('Tryserver master cannot be found because: %s\n'
3132 'Please manually specify the tryserver master'
3133 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003134
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003135 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003136 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003137 if not options.bot:
3138 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003139
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003140 # Get try masters from PRESUBMIT.py files.
3141 masters = presubmit_support.DoGetTryMasters(
3142 change,
3143 change.LocalPaths(),
3144 settings.GetRoot(),
3145 None,
3146 None,
3147 options.verbose,
3148 sys.stdout)
3149 if masters:
3150 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003151
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003152 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3153 options.bot = presubmit_support.DoGetTrySlaves(
3154 change,
3155 change.LocalPaths(),
3156 settings.GetRoot(),
3157 None,
3158 None,
3159 options.verbose,
3160 sys.stdout)
3161 if not options.bot:
3162 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003163
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003164 builders_and_tests = {}
3165 # TODO(machenbach): The old style command-line options don't support
3166 # multiple try masters yet.
3167 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3168 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3169
3170 for bot in old_style:
3171 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003172 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003173 elif ',' in bot:
3174 parser.error('Specify one bot per --bot flag')
3175 else:
3176 builders_and_tests.setdefault(bot, []).append('defaulttests')
3177
3178 for bot, tests in new_style:
3179 builders_and_tests.setdefault(bot, []).extend(tests)
3180
3181 # Return a master map with one master to be backwards compatible. The
3182 # master name defaults to an empty string, which will cause the master
3183 # not to be set on rietveld (deprecated).
3184 return {options.master: builders_and_tests}
3185
3186 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003187
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003188 for builders in masters.itervalues():
3189 if any('triggered' in b for b in builders):
3190 print >> sys.stderr, (
3191 'ERROR You are trying to send a job to a triggered bot. This type of'
3192 ' bot requires an\ninitial job from a parent (usually a builder). '
3193 'Instead send your job to the parent.\n'
3194 'Bot list: %s' % builders)
3195 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003196
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003197 patchset = cl.GetMostRecentPatchset()
3198 if patchset and patchset != cl.GetPatchset():
3199 print(
3200 '\nWARNING Mismatch between local config and server. Did a previous '
3201 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3202 'Continuing using\npatchset %s.\n' % patchset)
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003203 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003204 try:
3205 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3206 except BuildbucketResponseException as ex:
3207 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003208 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003209 except Exception as e:
3210 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3211 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3212 e, stacktrace)
3213 return 1
3214 else:
3215 try:
3216 cl.RpcServer().trigger_distributed_try_jobs(
3217 cl.GetIssue(), patchset, options.name, options.clobber,
3218 options.revision, masters)
3219 except urllib2.HTTPError as e:
3220 if e.code == 404:
3221 print('404 from rietveld; '
3222 'did you mean to use "git try" instead of "git cl try"?')
3223 return 1
3224 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003225
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003226 for (master, builders) in sorted(masters.iteritems()):
3227 if master:
3228 print 'Master: %s' % master
3229 length = max(len(builder) for builder in builders)
3230 for builder in sorted(builders):
3231 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003232 return 0
3233
3234
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003235@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003236def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003237 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003238 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003239 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003240 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003241
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003243 if args:
3244 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003245 branch = cl.GetBranch()
3246 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003247 cl = Changelist()
3248 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003249
3250 # Clear configured merge-base, if there is one.
3251 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003252 else:
3253 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 return 0
3255
3256
thestig@chromium.org00858c82013-12-02 23:08:03 +00003257def CMDweb(parser, args):
3258 """Opens the current CL in the web browser."""
3259 _, args = parser.parse_args(args)
3260 if args:
3261 parser.error('Unrecognized args: %s' % ' '.join(args))
3262
3263 issue_url = Changelist().GetIssueURL()
3264 if not issue_url:
3265 print >> sys.stderr, 'ERROR No issue to open'
3266 return 1
3267
3268 webbrowser.open(issue_url)
3269 return 0
3270
3271
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003272def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003273 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003274 auth.add_auth_options(parser)
3275 options, args = parser.parse_args(args)
3276 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003277 if args:
3278 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003279 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003280 props = cl.GetIssueProperties()
3281 if props.get('private'):
3282 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003283 cl.SetFlag('commit', '1')
3284 return 0
3285
3286
groby@chromium.org411034a2013-02-26 15:12:01 +00003287def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003288 """Closes the issue."""
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)
groby@chromium.org411034a2013-02-26 15:12:01 +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)
groby@chromium.org411034a2013-02-26 15:12:01 +00003295 # Ensure there actually is an issue to close.
3296 cl.GetDescription()
3297 cl.CloseIssue()
3298 return 0
3299
3300
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003301def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003302 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003303 auth.add_auth_options(parser)
3304 options, args = parser.parse_args(args)
3305 auth_config = auth.extract_auth_config_from_options(options)
3306 if args:
3307 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003308
3309 # Uncommitted (staged and unstaged) changes will be destroyed by
3310 # "git reset --hard" if there are merging conflicts in PatchIssue().
3311 # Staged changes would be committed along with the patch from last
3312 # upload, hence counted toward the "last upload" side in the final
3313 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003314 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003315 return 1
3316
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003317 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003318 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003319 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003320 if not issue:
3321 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003322 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003323 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003324
3325 # Create a new branch based on the merge-base
3326 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3327 try:
3328 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003329 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003330 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003331 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003332 return rtn
3333
wychen@chromium.org06928532015-02-03 02:11:29 +00003334 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003335 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003336 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003337 finally:
3338 RunGit(['checkout', '-q', branch])
3339 RunGit(['branch', '-D', TMP_BRANCH])
3340
3341 return 0
3342
3343
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003344def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003345 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003346 parser.add_option(
3347 '--no-color',
3348 action='store_true',
3349 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003350 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003351 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003352 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003353
3354 author = RunGit(['config', 'user.email']).strip() or None
3355
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003356 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003357
3358 if args:
3359 if len(args) > 1:
3360 parser.error('Unknown args')
3361 base_branch = args[0]
3362 else:
3363 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003364 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003365
3366 change = cl.GetChange(base_branch, None)
3367 return owners_finder.OwnersFinder(
3368 [f.LocalPath() for f in
3369 cl.GetChange(base_branch, None).AffectedFiles()],
3370 change.RepositoryRoot(), author,
3371 fopen=file, os_path=os.path, glob=glob.glob,
3372 disable_color=options.no_color).run()
3373
3374
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003375def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3376 """Generates a diff command."""
3377 # Generate diff for the current branch's changes.
3378 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3379 upstream_commit, '--' ]
3380
3381 if args:
3382 for arg in args:
3383 if os.path.isdir(arg):
3384 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3385 elif os.path.isfile(arg):
3386 diff_cmd.append(arg)
3387 else:
3388 DieWithError('Argument "%s" is not a file or a directory' % arg)
3389 else:
3390 diff_cmd.extend('*' + ext for ext in extensions)
3391
3392 return diff_cmd
3393
3394
enne@chromium.org555cfe42014-01-29 18:21:39 +00003395@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003396def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003397 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003398 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003399 parser.add_option('--full', action='store_true',
3400 help='Reformat the full content of all touched files')
3401 parser.add_option('--dry-run', action='store_true',
3402 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003403 parser.add_option('--python', action='store_true',
3404 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003405 parser.add_option('--diff', action='store_true',
3406 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003407 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003408
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003409 # git diff generates paths against the root of the repository. Change
3410 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003411 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003412 if rel_base_path:
3413 os.chdir(rel_base_path)
3414
digit@chromium.org29e47272013-05-17 17:01:46 +00003415 # Grab the merge-base commit, i.e. the upstream commit of the current
3416 # branch when it was created or the last time it was rebased. This is
3417 # to cover the case where the user may have called "git fetch origin",
3418 # moving the origin branch to a newer commit, but hasn't rebased yet.
3419 upstream_commit = None
3420 cl = Changelist()
3421 upstream_branch = cl.GetUpstreamBranch()
3422 if upstream_branch:
3423 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3424 upstream_commit = upstream_commit.strip()
3425
3426 if not upstream_commit:
3427 DieWithError('Could not find base commit for this branch. '
3428 'Are you in detached state?')
3429
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003430 if opts.full:
3431 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003432 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003433 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003434 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003435 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003436
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003437 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003438 diff_output = RunGit(diff_cmd)
3439
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003440 top_dir = os.path.normpath(
3441 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3442
3443 # Locate the clang-format binary in the checkout
3444 try:
3445 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3446 except clang_format.NotFoundError, e:
3447 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003448
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003449 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3450 # formatted. This is used to block during the presubmit.
3451 return_value = 0
3452
digit@chromium.org29e47272013-05-17 17:01:46 +00003453 if opts.full:
3454 # diff_output is a list of files to send to clang-format.
3455 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003456 if files:
3457 cmd = [clang_format_tool]
3458 if not opts.dry_run and not opts.diff:
3459 cmd.append('-i')
3460 stdout = RunCommand(cmd + files, cwd=top_dir)
3461 if opts.diff:
3462 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003463 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003464 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003465 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003466 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003467 try:
3468 script = clang_format.FindClangFormatScriptInChromiumTree(
3469 'clang-format-diff.py')
3470 except clang_format.NotFoundError, e:
3471 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003472
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003473 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003474 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003475 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003476
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003477 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003478 if opts.diff:
3479 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003480 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003481 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003482
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003483 # Similar code to above, but using yapf on .py files rather than clang-format
3484 # on C/C++ files
3485 if opts.python:
3486 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3487 diff_output = RunGit(diff_cmd)
3488 yapf_tool = gclient_utils.FindExecutable('yapf')
3489 if yapf_tool is None:
3490 DieWithError('yapf not found in PATH')
3491
3492 if opts.full:
3493 files = diff_output.splitlines()
3494 if files:
3495 cmd = [yapf_tool]
3496 if not opts.dry_run and not opts.diff:
3497 cmd.append('-i')
3498 stdout = RunCommand(cmd + files, cwd=top_dir)
3499 if opts.diff:
3500 sys.stdout.write(stdout)
3501 else:
3502 # TODO(sbc): yapf --lines mode still has some issues.
3503 # https://github.com/google/yapf/issues/154
3504 DieWithError('--python currently only works with --full')
3505
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003506 # Build a diff command that only operates on dart files. dart's formatter
3507 # does not have the nice property of only operating on modified chunks, so
3508 # hard code full.
3509 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3510 args, ['.dart'])
3511 dart_diff_output = RunGit(dart_diff_cmd)
3512 if dart_diff_output:
3513 try:
3514 command = [dart_format.FindDartFmtToolInChromiumTree()]
3515 if not opts.dry_run and not opts.diff:
3516 command.append('-w')
3517 command.extend(dart_diff_output.splitlines())
3518
3519 stdout = RunCommand(command, cwd=top_dir, env=env)
3520 if opts.dry_run and stdout:
3521 return_value = 2
3522 except dart_format.NotFoundError as e:
3523 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3524 'this checkout.')
3525
3526 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003527
3528
maruel@chromium.org29404b52014-09-08 22:58:00 +00003529def CMDlol(parser, args):
3530 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003531 print zlib.decompress(base64.b64decode(
3532 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3533 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3534 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3535 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003536 return 0
3537
3538
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003539class OptionParser(optparse.OptionParser):
3540 """Creates the option parse and add --verbose support."""
3541 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003542 optparse.OptionParser.__init__(
3543 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003544 self.add_option(
3545 '-v', '--verbose', action='count', default=0,
3546 help='Use 2 times for more debugging info')
3547
3548 def parse_args(self, args=None, values=None):
3549 options, args = optparse.OptionParser.parse_args(self, args, values)
3550 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3551 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3552 return options, args
3553
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003556 if sys.hexversion < 0x02060000:
3557 print >> sys.stderr, (
3558 '\nYour python version %s is unsupported, please upgrade.\n' %
3559 sys.version.split(' ', 1)[0])
3560 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003561
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003562 # Reload settings.
3563 global settings
3564 settings = Settings()
3565
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003566 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003567 dispatcher = subcommand.CommandDispatcher(__name__)
3568 try:
3569 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003570 except auth.AuthenticationError as e:
3571 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003572 except urllib2.HTTPError, e:
3573 if e.code != 500:
3574 raise
3575 DieWithError(
3576 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3577 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003578 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579
3580
3581if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003582 # These affect sys.stdout so do it outside of main() to simplify mocks in
3583 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003584 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003585 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003586 try:
3587 sys.exit(main(sys.argv[1:]))
3588 except KeyboardInterrupt:
3589 sys.stderr.write('interrupted\n')
3590 sys.exit(1)